Show More
@@ -14,6 +14,7 b' import traceback' | |||||
14 | import types |
|
14 | import types | |
15 | import warnings |
|
15 | import warnings | |
16 | from contextlib import contextmanager |
|
16 | from contextlib import contextmanager | |
|
17 | from pathlib import Path | |||
17 | from typing import Any |
|
18 | from typing import Any | |
18 | from typing import Callable |
|
19 | from typing import Callable | |
19 | from typing import Dict |
|
20 | from typing import Dict | |
@@ -28,8 +29,6 b' from typing import Type' | |||||
28 | from typing import TYPE_CHECKING |
|
29 | from typing import TYPE_CHECKING | |
29 | from typing import Union |
|
30 | from typing import Union | |
30 |
|
31 | |||
31 | import py.path |
|
|||
32 |
|
||||
33 | import pytest |
|
32 | import pytest | |
34 | from _pytest import outcomes |
|
33 | from _pytest import outcomes | |
35 | from _pytest._code.code import ExceptionInfo |
|
34 | from _pytest._code.code import ExceptionInfo | |
@@ -42,6 +41,7 b' from _pytest.config.argparsing import Parser' | |||||
42 | from _pytest.fixtures import FixtureRequest |
|
41 | from _pytest.fixtures import FixtureRequest | |
43 | from _pytest.nodes import Collector |
|
42 | from _pytest.nodes import Collector | |
44 | from _pytest.outcomes import OutcomeException |
|
43 | from _pytest.outcomes import OutcomeException | |
|
44 | from _pytest.pathlib import fnmatch_ex | |||
45 | from _pytest.pathlib import import_path |
|
45 | from _pytest.pathlib import import_path | |
46 | from _pytest.python_api import approx |
|
46 | from _pytest.python_api import approx | |
47 | from _pytest.warning_types import PytestWarning |
|
47 | from _pytest.warning_types import PytestWarning | |
@@ -126,35 +126,38 b' def pytest_unconfigure() -> None:' | |||||
126 |
|
126 | |||
127 |
|
127 | |||
128 | def pytest_collect_file( |
|
128 | def pytest_collect_file( | |
129 |
path: |
|
129 | file_path: Path, | |
130 | parent: Collector, |
|
130 | parent: Collector, | |
131 | ) -> Optional[Union["IPDoctestModule", "IPDoctestTextfile"]]: |
|
131 | ) -> Optional[Union["IPDoctestModule", "IPDoctestTextfile"]]: | |
132 | config = parent.config |
|
132 | config = parent.config | |
133 |
if path. |
|
133 | if file_path.suffix == ".py": | |
134 |
if config.option.ipdoctestmodules and not |
|
134 | if config.option.ipdoctestmodules and not any( | |
135 | mod: IPDoctestModule = IPDoctestModule.from_parent(parent, fspath=path) |
|
135 | (_is_setup_py(file_path), _is_main_py(file_path)) | |
|
136 | ): | |||
|
137 | mod: IPDoctestModule = IPDoctestModule.from_parent(parent, path=file_path) | |||
136 | return mod |
|
138 | return mod | |
137 | elif _is_ipdoctest(config, path, parent): |
|
139 | elif _is_ipdoctest(config, file_path, parent): | |
138 |
txt: IPDoctestTextfile = IPDoctestTextfile.from_parent(parent, |
|
140 | txt: IPDoctestTextfile = IPDoctestTextfile.from_parent(parent, path=file_path) | |
139 | return txt |
|
141 | return txt | |
140 | return None |
|
142 | return None | |
141 |
|
143 | |||
142 |
|
144 | |||
143 |
def _is_setup_py(path: |
|
145 | def _is_setup_py(path: Path) -> bool: | |
144 |
if path. |
|
146 | if path.name != "setup.py": | |
145 | return False |
|
147 | return False | |
146 |
contents = path.read_b |
|
148 | contents = path.read_bytes() | |
147 | return b"setuptools" in contents or b"distutils" in contents |
|
149 | return b"setuptools" in contents or b"distutils" in contents | |
148 |
|
150 | |||
149 |
|
151 | |||
150 |
def _is_ipdoctest(config: Config, path: |
|
152 | def _is_ipdoctest(config: Config, path: Path, parent: Collector) -> bool: | |
151 |
if path. |
|
153 | if path.suffix in (".txt", ".rst") and parent.session.isinitpath(path): | |
152 | return True |
|
154 | return True | |
153 | globs = config.getoption("ipdoctestglob") or ["test*.txt"] |
|
155 | globs = config.getoption("ipdoctestglob") or ["test*.txt"] | |
154 | for glob in globs: |
|
156 | return any(fnmatch_ex(glob, path) for glob in globs) | |
155 | if path.check(fnmatch=glob): |
|
157 | ||
156 | return True |
|
158 | ||
157 | return False |
|
159 | def _is_main_py(path: Path) -> bool: | |
|
160 | return path.name == "__main__.py" | |||
158 |
|
161 | |||
159 |
|
162 | |||
160 | class ReprFailDoctest(TerminalRepr): |
|
163 | class ReprFailDoctest(TerminalRepr): | |
@@ -273,7 +276,7 b' class IPDoctestItem(pytest.Item):' | |||||
273 | runner: "IPDocTestRunner", |
|
276 | runner: "IPDocTestRunner", | |
274 | dtest: "doctest.DocTest", |
|
277 | dtest: "doctest.DocTest", | |
275 | ): |
|
278 | ): | |
276 |
# incompatible signature due to |
|
279 | # incompatible signature due to imposed limits on subclass | |
277 | """The public named constructor.""" |
|
280 | """The public named constructor.""" | |
278 | return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest) |
|
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 | elif isinstance(excinfo.value, MultipleDoctestFailures): |
|
375 | elif isinstance(excinfo.value, MultipleDoctestFailures): | |
373 | failures = excinfo.value.failures |
|
376 | failures = excinfo.value.failures | |
374 |
|
377 | |||
375 |
if failures is |
|
378 | if failures is None: | |
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: |
|
|||
425 | return super().repr_failure(excinfo) |
|
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 | assert self.dtest is not None |
|
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 | def _get_flag_lookup() -> Dict[str, int]: |
|
431 | def _get_flag_lookup() -> Dict[str, int]: | |
@@ -474,9 +473,9 b' class IPDoctestTextfile(pytest.Module):' | |||||
474 | # Inspired by doctest.testfile; ideally we would use it directly, |
|
473 | # Inspired by doctest.testfile; ideally we would use it directly, | |
475 | # but it doesn't support passing a custom checker. |
|
474 | # but it doesn't support passing a custom checker. | |
476 | encoding = self.config.getini("ipdoctest_encoding") |
|
475 | encoding = self.config.getini("ipdoctest_encoding") | |
477 |
text = self. |
|
476 | text = self.path.read_text(encoding) | |
478 |
filename = str(self. |
|
477 | filename = str(self.path) | |
479 |
name = self. |
|
478 | name = self.path.name | |
480 | globs = {"__name__": "__main__"} |
|
479 | globs = {"__name__": "__main__"} | |
481 |
|
480 | |||
482 | optionflags = get_optionflags(self) |
|
481 | optionflags = get_optionflags(self) | |
@@ -559,15 +558,20 b' class IPDoctestModule(pytest.Module):' | |||||
559 |
|
558 | |||
560 | def _find_lineno(self, obj, source_lines): |
|
559 | def _find_lineno(self, obj, source_lines): | |
561 | """Doctest code does not take into account `@property`, this |
|
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 | if isinstance(obj, property): |
|
566 | if isinstance(obj, property): | |
567 | obj = getattr(obj, "fget", obj) |
|
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 | # Type ignored because this is a private function. |
|
573 | # Type ignored because this is a private function. | |
569 |
return |
|
574 | return super()._find_lineno( # type:ignore[misc] | |
570 | self, |
|
|||
571 | obj, |
|
575 | obj, | |
572 | source_lines, |
|
576 | source_lines, | |
573 | ) |
|
577 | ) | |
@@ -580,20 +584,22 b' class IPDoctestModule(pytest.Module):' | |||||
580 | with _patch_unwrap_mock_aware(): |
|
584 | with _patch_unwrap_mock_aware(): | |
581 |
|
585 | |||
582 | # Type ignored because this is a private function. |
|
586 | # Type ignored because this is a private function. | |
583 |
|
|
587 | super()._find( # type:ignore[misc] | |
584 |
|
|
588 | tests, obj, name, module, source_lines, globs, seen | |
585 | ) |
|
589 | ) | |
586 |
|
590 | |||
587 |
if self. |
|
591 | if self.path.name == "conftest.py": | |
588 | module = self.config.pluginmanager._importconftest( |
|
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 | else: |
|
597 | else: | |
592 | try: |
|
598 | try: | |
593 |
module = import_path(self. |
|
599 | module = import_path(self.path, root=self.config.rootpath) | |
594 | except ImportError: |
|
600 | except ImportError: | |
595 | if self.config.getvalue("ipdoctest_ignore_import_errors"): |
|
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 | else: |
|
603 | else: | |
598 | raise |
|
604 | raise | |
599 | # Uses internal doctest module parsing mechanism. |
|
605 | # Uses internal doctest module parsing mechanism. | |
@@ -665,7 +671,7 b' def _init_checker_class() -> Type["IPDoctestOutputChecker"]:' | |||||
665 | ) |
|
671 | ) | |
666 |
|
672 | |||
667 | def check_output(self, want: str, got: str, optionflags: int) -> bool: |
|
673 | def check_output(self, want: str, got: str, optionflags: int) -> bool: | |
668 |
if |
|
674 | if super().check_output(want, got, optionflags): | |
669 | return True |
|
675 | return True | |
670 |
|
676 | |||
671 | allow_unicode = optionflags & _get_allow_unicode_flag() |
|
677 | allow_unicode = optionflags & _get_allow_unicode_flag() | |
@@ -689,7 +695,7 b' def _init_checker_class() -> Type["IPDoctestOutputChecker"]:' | |||||
689 | if allow_number: |
|
695 | if allow_number: | |
690 | got = self._remove_unwanted_precision(want, got) |
|
696 | got = self._remove_unwanted_precision(want, got) | |
691 |
|
697 | |||
692 |
return |
|
698 | return super().check_output(want, got, optionflags) | |
693 |
|
699 | |||
694 | def _remove_unwanted_precision(self, want: str, got: str) -> str: |
|
700 | def _remove_unwanted_precision(self, want: str, got: str) -> str: | |
695 | wants = list(self._number_re.finditer(want)) |
|
701 | wants = list(self._number_re.finditer(want)) | |
@@ -702,13 +708,10 b' def _init_checker_class() -> Type["IPDoctestOutputChecker"]:' | |||||
702 | exponent: Optional[str] = w.group("exponent1") |
|
708 | exponent: Optional[str] = w.group("exponent1") | |
703 | if exponent is None: |
|
709 | if exponent is None: | |
704 | exponent = w.group("exponent2") |
|
710 | exponent = w.group("exponent2") | |
705 |
if fraction is None |
|
711 | precision = 0 if fraction is None else len(fraction) | |
706 | precision = 0 |
|
|||
707 | else: |
|
|||
708 | precision = len(fraction) |
|
|||
709 | if exponent is not None: |
|
712 | if exponent is not None: | |
710 | precision -= int(exponent) |
|
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 | # They're close enough. Replace the text we actually |
|
715 | # They're close enough. Replace the text we actually | |
713 | # got with the text we want, so that it will match when we |
|
716 | # got with the text we want, so that it will match when we | |
714 | # check the string literally. |
|
717 | # check the string literally. |
General Comments 0
You need to be logged in to leave comments.
Login now