Show More
@@ -14,6 +14,7 b' import traceback' | |||
|
14 | 14 | import types |
|
15 | 15 | import warnings |
|
16 | 16 | from contextlib import contextmanager |
|
17 | from pathlib import Path | |
|
17 | 18 | from typing import Any |
|
18 | 19 | from typing import Callable |
|
19 | 20 | from typing import Dict |
@@ -28,8 +29,6 b' from typing import Type' | |||
|
28 | 29 | from typing import TYPE_CHECKING |
|
29 | 30 | from typing import Union |
|
30 | 31 | |
|
31 | import py.path | |
|
32 | ||
|
33 | 32 | import pytest |
|
34 | 33 | from _pytest import outcomes |
|
35 | 34 | from _pytest._code.code import ExceptionInfo |
@@ -42,6 +41,7 b' from _pytest.config.argparsing import Parser' | |||
|
42 | 41 | from _pytest.fixtures import FixtureRequest |
|
43 | 42 | from _pytest.nodes import Collector |
|
44 | 43 | from _pytest.outcomes import OutcomeException |
|
44 | from _pytest.pathlib import fnmatch_ex | |
|
45 | 45 | from _pytest.pathlib import import_path |
|
46 | 46 | from _pytest.python_api import approx |
|
47 | 47 | from _pytest.warning_types import PytestWarning |
@@ -126,35 +126,38 b' def pytest_unconfigure() -> None:' | |||
|
126 | 126 | |
|
127 | 127 | |
|
128 | 128 | def pytest_collect_file( |
|
129 |
path: |
|
|
129 | file_path: Path, | |
|
130 | 130 | parent: Collector, |
|
131 | 131 | ) -> Optional[Union["IPDoctestModule", "IPDoctestTextfile"]]: |
|
132 | 132 | config = parent.config |
|
133 |
if path. |
|
|
134 |
if config.option.ipdoctestmodules and not |
|
|
135 | mod: IPDoctestModule = IPDoctestModule.from_parent(parent, fspath=path) | |
|
133 | if file_path.suffix == ".py": | |
|
134 | if config.option.ipdoctestmodules and not any( | |
|
135 | (_is_setup_py(file_path), _is_main_py(file_path)) | |
|
136 | ): | |
|
137 | mod: IPDoctestModule = IPDoctestModule.from_parent(parent, path=file_path) | |
|
136 | 138 | return mod |
|
137 | elif _is_ipdoctest(config, path, parent): | |
|
138 |
txt: IPDoctestTextfile = IPDoctestTextfile.from_parent(parent, |
|
|
139 | elif _is_ipdoctest(config, file_path, parent): | |
|
140 | txt: IPDoctestTextfile = IPDoctestTextfile.from_parent(parent, path=file_path) | |
|
139 | 141 | return txt |
|
140 | 142 | return None |
|
141 | 143 | |
|
142 | 144 | |
|
143 |
def _is_setup_py(path: |
|
|
144 |
if path. |
|
|
145 | def _is_setup_py(path: Path) -> bool: | |
|
146 | if path.name != "setup.py": | |
|
145 | 147 | return False |
|
146 |
contents = path.read_b |
|
|
148 | contents = path.read_bytes() | |
|
147 | 149 | return b"setuptools" in contents or b"distutils" in contents |
|
148 | 150 | |
|
149 | 151 | |
|
150 |
def _is_ipdoctest(config: Config, path: |
|
|
151 |
if path. |
|
|
152 | def _is_ipdoctest(config: Config, path: Path, parent: Collector) -> bool: | |
|
153 | if path.suffix in (".txt", ".rst") and parent.session.isinitpath(path): | |
|
152 | 154 | return True |
|
153 | 155 | globs = config.getoption("ipdoctestglob") or ["test*.txt"] |
|
154 | for glob in globs: | |
|
155 | if path.check(fnmatch=glob): | |
|
156 | return True | |
|
157 | return False | |
|
156 | return any(fnmatch_ex(glob, path) for glob in globs) | |
|
157 | ||
|
158 | ||
|
159 | def _is_main_py(path: Path) -> bool: | |
|
160 | return path.name == "__main__.py" | |
|
158 | 161 | |
|
159 | 162 | |
|
160 | 163 | class ReprFailDoctest(TerminalRepr): |
@@ -273,7 +276,7 b' class IPDoctestItem(pytest.Item):' | |||
|
273 | 276 | runner: "IPDocTestRunner", |
|
274 | 277 | dtest: "doctest.DocTest", |
|
275 | 278 | ): |
|
276 |
# incompatible signature due to |
|
|
279 | # incompatible signature due to imposed limits on subclass | |
|
277 | 280 | """The public named constructor.""" |
|
278 | 281 | return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest) |
|
279 | 282 | |
@@ -372,61 +375,57 b' class IPDoctestItem(pytest.Item):' | |||
|
372 | 375 | elif isinstance(excinfo.value, MultipleDoctestFailures): |
|
373 | 376 | failures = excinfo.value.failures |
|
374 | 377 | |
|
375 |
if failures is |
|
|
376 | reprlocation_lines = [] | |
|
377 | for failure in failures: | |
|
378 | example = failure.example | |
|
379 | test = failure.test | |
|
380 | filename = test.filename | |
|
381 | if test.lineno is None: | |
|
382 | lineno = None | |
|
383 | else: | |
|
384 | lineno = test.lineno + example.lineno + 1 | |
|
385 | message = type(failure).__name__ | |
|
386 | # TODO: ReprFileLocation doesn't expect a None lineno. | |
|
387 | reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type] | |
|
388 | checker = _get_checker() | |
|
389 | report_choice = _get_report_choice( | |
|
390 | self.config.getoption("ipdoctestreport") | |
|
391 | ) | |
|
392 | if lineno is not None: | |
|
393 | assert failure.test.docstring is not None | |
|
394 | lines = failure.test.docstring.splitlines(False) | |
|
395 | # add line numbers to the left of the error message | |
|
396 | assert test.lineno is not None | |
|
397 | lines = [ | |
|
398 | "%03d %s" % (i + test.lineno + 1, x) | |
|
399 | for (i, x) in enumerate(lines) | |
|
400 | ] | |
|
401 | # trim docstring error lines to 10 | |
|
402 | lines = lines[max(example.lineno - 9, 0) : example.lineno + 1] | |
|
403 | else: | |
|
404 | lines = [ | |
|
405 | "EXAMPLE LOCATION UNKNOWN, not showing all tests of that example" | |
|
406 | ] | |
|
407 | indent = ">>>" | |
|
408 | for line in example.source.splitlines(): | |
|
409 | lines.append(f"??? {indent} {line}") | |
|
410 | indent = "..." | |
|
411 | if isinstance(failure, doctest.DocTestFailure): | |
|
412 | lines += checker.output_difference( | |
|
413 | example, failure.got, report_choice | |
|
414 | ).split("\n") | |
|
415 | else: | |
|
416 | inner_excinfo = ExceptionInfo(failure.exc_info) | |
|
417 | lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)] | |
|
418 | lines += [ | |
|
419 | x.strip("\n") | |
|
420 | for x in traceback.format_exception(*failure.exc_info) | |
|
421 | ] | |
|
422 | reprlocation_lines.append((reprlocation, lines)) | |
|
423 | return ReprFailDoctest(reprlocation_lines) | |
|
424 | else: | |
|
378 | if failures is None: | |
|
425 | 379 | return super().repr_failure(excinfo) |
|
426 | 380 | |
|
427 | def reportinfo(self): | |
|
381 | reprlocation_lines = [] | |
|
382 | for failure in failures: | |
|
383 | example = failure.example | |
|
384 | test = failure.test | |
|
385 | filename = test.filename | |
|
386 | if test.lineno is None: | |
|
387 | lineno = None | |
|
388 | else: | |
|
389 | lineno = test.lineno + example.lineno + 1 | |
|
390 | message = type(failure).__name__ | |
|
391 | # TODO: ReprFileLocation doesn't expect a None lineno. | |
|
392 | reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type] | |
|
393 | checker = _get_checker() | |
|
394 | report_choice = _get_report_choice(self.config.getoption("ipdoctestreport")) | |
|
395 | if lineno is not None: | |
|
396 | assert failure.test.docstring is not None | |
|
397 | lines = failure.test.docstring.splitlines(False) | |
|
398 | # add line numbers to the left of the error message | |
|
399 | assert test.lineno is not None | |
|
400 | lines = [ | |
|
401 | "%03d %s" % (i + test.lineno + 1, x) for (i, x) in enumerate(lines) | |
|
402 | ] | |
|
403 | # trim docstring error lines to 10 | |
|
404 | lines = lines[max(example.lineno - 9, 0) : example.lineno + 1] | |
|
405 | else: | |
|
406 | lines = [ | |
|
407 | "EXAMPLE LOCATION UNKNOWN, not showing all tests of that example" | |
|
408 | ] | |
|
409 | indent = ">>>" | |
|
410 | for line in example.source.splitlines(): | |
|
411 | lines.append(f"??? {indent} {line}") | |
|
412 | indent = "..." | |
|
413 | if isinstance(failure, doctest.DocTestFailure): | |
|
414 | lines += checker.output_difference( | |
|
415 | example, failure.got, report_choice | |
|
416 | ).split("\n") | |
|
417 | else: | |
|
418 | inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info) | |
|
419 | lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)] | |
|
420 | lines += [ | |
|
421 | x.strip("\n") for x in traceback.format_exception(*failure.exc_info) | |
|
422 | ] | |
|
423 | reprlocation_lines.append((reprlocation, lines)) | |
|
424 | return ReprFailDoctest(reprlocation_lines) | |
|
425 | ||
|
426 | def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]: | |
|
428 | 427 | assert self.dtest is not None |
|
429 |
return self. |
|
|
428 | return self.path, self.dtest.lineno, "[ipdoctest] %s" % self.name | |
|
430 | 429 | |
|
431 | 430 | |
|
432 | 431 | def _get_flag_lookup() -> Dict[str, int]: |
@@ -474,9 +473,9 b' class IPDoctestTextfile(pytest.Module):' | |||
|
474 | 473 | # Inspired by doctest.testfile; ideally we would use it directly, |
|
475 | 474 | # but it doesn't support passing a custom checker. |
|
476 | 475 | encoding = self.config.getini("ipdoctest_encoding") |
|
477 |
text = self. |
|
|
478 |
filename = str(self. |
|
|
479 |
name = self. |
|
|
476 | text = self.path.read_text(encoding) | |
|
477 | filename = str(self.path) | |
|
478 | name = self.path.name | |
|
480 | 479 | globs = {"__name__": "__main__"} |
|
481 | 480 | |
|
482 | 481 | optionflags = get_optionflags(self) |
@@ -559,15 +558,20 b' class IPDoctestModule(pytest.Module):' | |||
|
559 | 558 | |
|
560 | 559 | def _find_lineno(self, obj, source_lines): |
|
561 | 560 | """Doctest code does not take into account `@property`, this |
|
562 | is a hackish way to fix it. | |
|
561 | is a hackish way to fix it. https://bugs.python.org/issue17446 | |
|
563 | 562 | |
|
564 | https://bugs.python.org/issue17446 | |
|
563 | Wrapped Doctests will need to be unwrapped so the correct | |
|
564 | line number is returned. This will be reported upstream. #8796 | |
|
565 | 565 | """ |
|
566 | 566 | if isinstance(obj, property): |
|
567 | 567 | obj = getattr(obj, "fget", obj) |
|
568 | ||
|
569 | if hasattr(obj, "__wrapped__"): | |
|
570 | # Get the main obj in case of it being wrapped | |
|
571 | obj = inspect.unwrap(obj) | |
|
572 | ||
|
568 | 573 | # Type ignored because this is a private function. |
|
569 |
return |
|
|
570 | self, | |
|
574 | return super()._find_lineno( # type:ignore[misc] | |
|
571 | 575 | obj, |
|
572 | 576 | source_lines, |
|
573 | 577 | ) |
@@ -580,20 +584,22 b' class IPDoctestModule(pytest.Module):' | |||
|
580 | 584 | with _patch_unwrap_mock_aware(): |
|
581 | 585 | |
|
582 | 586 | # Type ignored because this is a private function. |
|
583 |
|
|
|
584 |
|
|
|
587 | super()._find( # type:ignore[misc] | |
|
588 | tests, obj, name, module, source_lines, globs, seen | |
|
585 | 589 | ) |
|
586 | 590 | |
|
587 |
if self. |
|
|
591 | if self.path.name == "conftest.py": | |
|
588 | 592 | module = self.config.pluginmanager._importconftest( |
|
589 | self.fspath, self.config.getoption("importmode") | |
|
593 | self.path, | |
|
594 | self.config.getoption("importmode"), | |
|
595 | rootpath=self.config.rootpath, | |
|
590 | 596 | ) |
|
591 | 597 | else: |
|
592 | 598 | try: |
|
593 |
module = import_path(self. |
|
|
599 | module = import_path(self.path, root=self.config.rootpath) | |
|
594 | 600 | except ImportError: |
|
595 | 601 | if self.config.getvalue("ipdoctest_ignore_import_errors"): |
|
596 |
pytest.skip("unable to import module %r" % self. |
|
|
602 | pytest.skip("unable to import module %r" % self.path) | |
|
597 | 603 | else: |
|
598 | 604 | raise |
|
599 | 605 | # Uses internal doctest module parsing mechanism. |
@@ -665,7 +671,7 b' def _init_checker_class() -> Type["IPDoctestOutputChecker"]:' | |||
|
665 | 671 | ) |
|
666 | 672 | |
|
667 | 673 | def check_output(self, want: str, got: str, optionflags: int) -> bool: |
|
668 |
if |
|
|
674 | if super().check_output(want, got, optionflags): | |
|
669 | 675 | return True |
|
670 | 676 | |
|
671 | 677 | allow_unicode = optionflags & _get_allow_unicode_flag() |
@@ -689,7 +695,7 b' def _init_checker_class() -> Type["IPDoctestOutputChecker"]:' | |||
|
689 | 695 | if allow_number: |
|
690 | 696 | got = self._remove_unwanted_precision(want, got) |
|
691 | 697 | |
|
692 |
return |
|
|
698 | return super().check_output(want, got, optionflags) | |
|
693 | 699 | |
|
694 | 700 | def _remove_unwanted_precision(self, want: str, got: str) -> str: |
|
695 | 701 | wants = list(self._number_re.finditer(want)) |
@@ -702,13 +708,10 b' def _init_checker_class() -> Type["IPDoctestOutputChecker"]:' | |||
|
702 | 708 | exponent: Optional[str] = w.group("exponent1") |
|
703 | 709 | if exponent is None: |
|
704 | 710 | exponent = w.group("exponent2") |
|
705 |
if fraction is None |
|
|
706 | precision = 0 | |
|
707 | else: | |
|
708 | precision = len(fraction) | |
|
711 | precision = 0 if fraction is None else len(fraction) | |
|
709 | 712 | if exponent is not None: |
|
710 | 713 | precision -= int(exponent) |
|
711 |
if float(w.group()) == approx(float(g.group()), abs=10 |
|
|
714 | if float(w.group()) == approx(float(g.group()), abs=10**-precision): | |
|
712 | 715 | # They're close enough. Replace the text we actually |
|
713 | 716 | # got with the text we want, so that it will match when we |
|
714 | 717 | # check the string literally. |
General Comments 0
You need to be logged in to leave comments.
Login now