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,55 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: py.path.local) -> bool: |
|
145 | if int(pytest.__version__.split(".")[0]) < 7: | |
144 | if path.basename != "setup.py": |
|
146 | _collect_file = pytest_collect_file | |
|
147 | ||||
|
148 | def pytest_collect_file( | |||
|
149 | path, | |||
|
150 | parent: Collector, | |||
|
151 | ) -> Optional[Union["IPDoctestModule", "IPDoctestTextfile"]]: | |||
|
152 | return _collect_file(Path(path), parent) | |||
|
153 | ||||
|
154 | _import_path = import_path | |||
|
155 | ||||
|
156 | def import_path(path, root): | |||
|
157 | import py.path | |||
|
158 | ||||
|
159 | return _import_path(py.path.local(path)) | |||
|
160 | ||||
|
161 | ||||
|
162 | def _is_setup_py(path: Path) -> bool: | |||
|
163 | if path.name != "setup.py": | |||
145 | return False |
|
164 | return False | |
146 |
contents = path.read_b |
|
165 | contents = path.read_bytes() | |
147 | return b"setuptools" in contents or b"distutils" in contents |
|
166 | return b"setuptools" in contents or b"distutils" in contents | |
148 |
|
167 | |||
149 |
|
168 | |||
150 |
def _is_ipdoctest(config: Config, path: |
|
169 | def _is_ipdoctest(config: Config, path: Path, parent: Collector) -> bool: | |
151 |
if path. |
|
170 | if path.suffix in (".txt", ".rst") and parent.session.isinitpath(path): | |
152 | return True |
|
171 | return True | |
153 | globs = config.getoption("ipdoctestglob") or ["test*.txt"] |
|
172 | globs = config.getoption("ipdoctestglob") or ["test*.txt"] | |
154 | for glob in globs: |
|
173 | return any(fnmatch_ex(glob, path) for glob in globs) | |
155 | if path.check(fnmatch=glob): |
|
174 | ||
156 | return True |
|
175 | ||
157 | return False |
|
176 | def _is_main_py(path: Path) -> bool: | |
|
177 | return path.name == "__main__.py" | |||
158 |
|
178 | |||
159 |
|
179 | |||
160 | class ReprFailDoctest(TerminalRepr): |
|
180 | class ReprFailDoctest(TerminalRepr): | |
@@ -273,7 +293,7 b' class IPDoctestItem(pytest.Item):' | |||||
273 | runner: "IPDocTestRunner", |
|
293 | runner: "IPDocTestRunner", | |
274 | dtest: "doctest.DocTest", |
|
294 | dtest: "doctest.DocTest", | |
275 | ): |
|
295 | ): | |
276 |
# incompatible signature due to |
|
296 | # incompatible signature due to imposed limits on subclass | |
277 | """The public named constructor.""" |
|
297 | """The public named constructor.""" | |
278 | return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest) |
|
298 | return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest) | |
279 |
|
299 | |||
@@ -372,61 +392,63 b' class IPDoctestItem(pytest.Item):' | |||||
372 | elif isinstance(excinfo.value, MultipleDoctestFailures): |
|
392 | elif isinstance(excinfo.value, MultipleDoctestFailures): | |
373 | failures = excinfo.value.failures |
|
393 | failures = excinfo.value.failures | |
374 |
|
394 | |||
375 |
if failures is |
|
395 | 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) |
|
396 | return super().repr_failure(excinfo) | |
426 |
|
397 | |||
427 | def reportinfo(self): |
|
398 | reprlocation_lines = [] | |
|
399 | for failure in failures: | |||
|
400 | example = failure.example | |||
|
401 | test = failure.test | |||
|
402 | filename = test.filename | |||
|
403 | if test.lineno is None: | |||
|
404 | lineno = None | |||
|
405 | else: | |||
|
406 | lineno = test.lineno + example.lineno + 1 | |||
|
407 | message = type(failure).__name__ | |||
|
408 | # TODO: ReprFileLocation doesn't expect a None lineno. | |||
|
409 | reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type] | |||
|
410 | checker = _get_checker() | |||
|
411 | report_choice = _get_report_choice(self.config.getoption("ipdoctestreport")) | |||
|
412 | if lineno is not None: | |||
|
413 | assert failure.test.docstring is not None | |||
|
414 | lines = failure.test.docstring.splitlines(False) | |||
|
415 | # add line numbers to the left of the error message | |||
|
416 | assert test.lineno is not None | |||
|
417 | lines = [ | |||
|
418 | "%03d %s" % (i + test.lineno + 1, x) for (i, x) in enumerate(lines) | |||
|
419 | ] | |||
|
420 | # trim docstring error lines to 10 | |||
|
421 | lines = lines[max(example.lineno - 9, 0) : example.lineno + 1] | |||
|
422 | else: | |||
|
423 | lines = [ | |||
|
424 | "EXAMPLE LOCATION UNKNOWN, not showing all tests of that example" | |||
|
425 | ] | |||
|
426 | indent = ">>>" | |||
|
427 | for line in example.source.splitlines(): | |||
|
428 | lines.append(f"??? {indent} {line}") | |||
|
429 | indent = "..." | |||
|
430 | if isinstance(failure, doctest.DocTestFailure): | |||
|
431 | lines += checker.output_difference( | |||
|
432 | example, failure.got, report_choice | |||
|
433 | ).split("\n") | |||
|
434 | else: | |||
|
435 | inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info) | |||
|
436 | lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)] | |||
|
437 | lines += [ | |||
|
438 | x.strip("\n") for x in traceback.format_exception(*failure.exc_info) | |||
|
439 | ] | |||
|
440 | reprlocation_lines.append((reprlocation, lines)) | |||
|
441 | return ReprFailDoctest(reprlocation_lines) | |||
|
442 | ||||
|
443 | def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]: | |||
428 | assert self.dtest is not None |
|
444 | assert self.dtest is not None | |
429 |
return self. |
|
445 | return self.path, self.dtest.lineno, "[ipdoctest] %s" % self.name | |
|
446 | ||||
|
447 | if int(pytest.__version__.split(".")[0]) < 7: | |||
|
448 | ||||
|
449 | @property | |||
|
450 | def path(self) -> Path: | |||
|
451 | return Path(self.fspath) | |||
430 |
|
452 | |||
431 |
|
453 | |||
432 | def _get_flag_lookup() -> Dict[str, int]: |
|
454 | def _get_flag_lookup() -> Dict[str, int]: | |
@@ -474,9 +496,9 b' class IPDoctestTextfile(pytest.Module):' | |||||
474 | # Inspired by doctest.testfile; ideally we would use it directly, |
|
496 | # Inspired by doctest.testfile; ideally we would use it directly, | |
475 | # but it doesn't support passing a custom checker. |
|
497 | # but it doesn't support passing a custom checker. | |
476 | encoding = self.config.getini("ipdoctest_encoding") |
|
498 | encoding = self.config.getini("ipdoctest_encoding") | |
477 |
text = self. |
|
499 | text = self.path.read_text(encoding) | |
478 |
filename = str(self. |
|
500 | filename = str(self.path) | |
479 |
name = self. |
|
501 | name = self.path.name | |
480 | globs = {"__name__": "__main__"} |
|
502 | globs = {"__name__": "__main__"} | |
481 |
|
503 | |||
482 | optionflags = get_optionflags(self) |
|
504 | optionflags = get_optionflags(self) | |
@@ -495,6 +517,27 b' class IPDoctestTextfile(pytest.Module):' | |||||
495 | self, name=test.name, runner=runner, dtest=test |
|
517 | self, name=test.name, runner=runner, dtest=test | |
496 | ) |
|
518 | ) | |
497 |
|
519 | |||
|
520 | if int(pytest.__version__.split(".")[0]) < 7: | |||
|
521 | ||||
|
522 | @property | |||
|
523 | def path(self) -> Path: | |||
|
524 | return Path(self.fspath) | |||
|
525 | ||||
|
526 | @classmethod | |||
|
527 | def from_parent( | |||
|
528 | cls, | |||
|
529 | parent, | |||
|
530 | *, | |||
|
531 | fspath=None, | |||
|
532 | path: Optional[Path] = None, | |||
|
533 | **kw, | |||
|
534 | ): | |||
|
535 | if path is not None: | |||
|
536 | import py.path | |||
|
537 | ||||
|
538 | fspath = py.path.local(path) | |||
|
539 | return super().from_parent(parent=parent, fspath=fspath, **kw) | |||
|
540 | ||||
498 |
|
541 | |||
499 | def _check_all_skipped(test: "doctest.DocTest") -> None: |
|
542 | def _check_all_skipped(test: "doctest.DocTest") -> None: | |
500 | """Raise pytest.skip() if all examples in the given DocTest have the SKIP |
|
543 | """Raise pytest.skip() if all examples in the given DocTest have the SKIP | |
@@ -559,15 +602,20 b' class IPDoctestModule(pytest.Module):' | |||||
559 |
|
602 | |||
560 | def _find_lineno(self, obj, source_lines): |
|
603 | def _find_lineno(self, obj, source_lines): | |
561 | """Doctest code does not take into account `@property`, this |
|
604 | """Doctest code does not take into account `@property`, this | |
562 | is a hackish way to fix it. |
|
605 | is a hackish way to fix it. https://bugs.python.org/issue17446 | |
563 |
|
606 | |||
564 | https://bugs.python.org/issue17446 |
|
607 | Wrapped Doctests will need to be unwrapped so the correct | |
|
608 | line number is returned. This will be reported upstream. #8796 | |||
565 | """ |
|
609 | """ | |
566 | if isinstance(obj, property): |
|
610 | if isinstance(obj, property): | |
567 | obj = getattr(obj, "fget", obj) |
|
611 | obj = getattr(obj, "fget", obj) | |
|
612 | ||||
|
613 | if hasattr(obj, "__wrapped__"): | |||
|
614 | # Get the main obj in case of it being wrapped | |||
|
615 | obj = inspect.unwrap(obj) | |||
|
616 | ||||
568 | # Type ignored because this is a private function. |
|
617 | # Type ignored because this is a private function. | |
569 |
return |
|
618 | return super()._find_lineno( # type:ignore[misc] | |
570 | self, |
|
|||
571 | obj, |
|
619 | obj, | |
572 | source_lines, |
|
620 | source_lines, | |
573 | ) |
|
621 | ) | |
@@ -580,20 +628,28 b' class IPDoctestModule(pytest.Module):' | |||||
580 | with _patch_unwrap_mock_aware(): |
|
628 | with _patch_unwrap_mock_aware(): | |
581 |
|
629 | |||
582 | # Type ignored because this is a private function. |
|
630 | # Type ignored because this is a private function. | |
583 |
|
|
631 | super()._find( # type:ignore[misc] | |
584 |
|
|
632 | tests, obj, name, module, source_lines, globs, seen | |
585 | ) |
|
633 | ) | |
586 |
|
634 | |||
587 |
if self. |
|
635 | if self.path.name == "conftest.py": | |
588 | module = self.config.pluginmanager._importconftest( |
|
636 | if int(pytest.__version__.split(".")[0]) < 7: | |
589 |
|
|
637 | module = self.config.pluginmanager._importconftest( | |
590 | ) |
|
638 | self.path, | |
|
639 | self.config.getoption("importmode"), | |||
|
640 | ) | |||
|
641 | else: | |||
|
642 | module = self.config.pluginmanager._importconftest( | |||
|
643 | self.path, | |||
|
644 | self.config.getoption("importmode"), | |||
|
645 | rootpath=self.config.rootpath, | |||
|
646 | ) | |||
591 | else: |
|
647 | else: | |
592 | try: |
|
648 | try: | |
593 |
module = import_path(self. |
|
649 | module = import_path(self.path, root=self.config.rootpath) | |
594 | except ImportError: |
|
650 | except ImportError: | |
595 | if self.config.getvalue("ipdoctest_ignore_import_errors"): |
|
651 | if self.config.getvalue("ipdoctest_ignore_import_errors"): | |
596 |
pytest.skip("unable to import module %r" % self. |
|
652 | pytest.skip("unable to import module %r" % self.path) | |
597 | else: |
|
653 | else: | |
598 | raise |
|
654 | raise | |
599 | # Uses internal doctest module parsing mechanism. |
|
655 | # Uses internal doctest module parsing mechanism. | |
@@ -612,6 +668,27 b' class IPDoctestModule(pytest.Module):' | |||||
612 | self, name=test.name, runner=runner, dtest=test |
|
668 | self, name=test.name, runner=runner, dtest=test | |
613 | ) |
|
669 | ) | |
614 |
|
670 | |||
|
671 | if int(pytest.__version__.split(".")[0]) < 7: | |||
|
672 | ||||
|
673 | @property | |||
|
674 | def path(self) -> Path: | |||
|
675 | return Path(self.fspath) | |||
|
676 | ||||
|
677 | @classmethod | |||
|
678 | def from_parent( | |||
|
679 | cls, | |||
|
680 | parent, | |||
|
681 | *, | |||
|
682 | fspath=None, | |||
|
683 | path: Optional[Path] = None, | |||
|
684 | **kw, | |||
|
685 | ): | |||
|
686 | if path is not None: | |||
|
687 | import py.path | |||
|
688 | ||||
|
689 | fspath = py.path.local(path) | |||
|
690 | return super().from_parent(parent=parent, fspath=fspath, **kw) | |||
|
691 | ||||
615 |
|
692 | |||
616 | def _setup_fixtures(doctest_item: IPDoctestItem) -> FixtureRequest: |
|
693 | def _setup_fixtures(doctest_item: IPDoctestItem) -> FixtureRequest: | |
617 | """Used by IPDoctestTextfile and IPDoctestItem to setup fixture information.""" |
|
694 | """Used by IPDoctestTextfile and IPDoctestItem to setup fixture information.""" | |
@@ -665,7 +742,7 b' def _init_checker_class() -> Type["IPDoctestOutputChecker"]:' | |||||
665 | ) |
|
742 | ) | |
666 |
|
743 | |||
667 | def check_output(self, want: str, got: str, optionflags: int) -> bool: |
|
744 | def check_output(self, want: str, got: str, optionflags: int) -> bool: | |
668 |
if |
|
745 | if super().check_output(want, got, optionflags): | |
669 | return True |
|
746 | return True | |
670 |
|
747 | |||
671 | allow_unicode = optionflags & _get_allow_unicode_flag() |
|
748 | allow_unicode = optionflags & _get_allow_unicode_flag() | |
@@ -689,7 +766,7 b' def _init_checker_class() -> Type["IPDoctestOutputChecker"]:' | |||||
689 | if allow_number: |
|
766 | if allow_number: | |
690 | got = self._remove_unwanted_precision(want, got) |
|
767 | got = self._remove_unwanted_precision(want, got) | |
691 |
|
768 | |||
692 |
return |
|
769 | return super().check_output(want, got, optionflags) | |
693 |
|
770 | |||
694 | def _remove_unwanted_precision(self, want: str, got: str) -> str: |
|
771 | def _remove_unwanted_precision(self, want: str, got: str) -> str: | |
695 | wants = list(self._number_re.finditer(want)) |
|
772 | wants = list(self._number_re.finditer(want)) | |
@@ -702,10 +779,7 b' def _init_checker_class() -> Type["IPDoctestOutputChecker"]:' | |||||
702 | exponent: Optional[str] = w.group("exponent1") |
|
779 | exponent: Optional[str] = w.group("exponent1") | |
703 | if exponent is None: |
|
780 | if exponent is None: | |
704 | exponent = w.group("exponent2") |
|
781 | exponent = w.group("exponent2") | |
705 |
if fraction is None |
|
782 | precision = 0 if fraction is None else len(fraction) | |
706 | precision = 0 |
|
|||
707 | else: |
|
|||
708 | precision = len(fraction) |
|
|||
709 | if exponent is not None: |
|
783 | if exponent is not None: | |
710 | precision -= int(exponent) |
|
784 | precision -= int(exponent) | |
711 | if float(w.group()) == approx(float(g.group()), abs=10 ** -precision): |
|
785 | if float(w.group()) == approx(float(g.group()), abs=10 ** -precision): |
@@ -63,7 +63,7 b' qtconsole =' | |||||
63 | qtconsole |
|
63 | qtconsole | |
64 | terminal = |
|
64 | terminal = | |
65 | test = |
|
65 | test = | |
66 |
pytest |
|
66 | pytest | |
67 | pytest-asyncio |
|
67 | pytest-asyncio | |
68 | testpath |
|
68 | testpath | |
69 | test_extra = |
|
69 | test_extra = | |
@@ -72,7 +72,7 b' test_extra =' | |||||
72 | nbformat |
|
72 | nbformat | |
73 | numpy>=1.19 |
|
73 | numpy>=1.19 | |
74 | pandas |
|
74 | pandas | |
75 |
pytest |
|
75 | pytest | |
76 | testpath |
|
76 | testpath | |
77 | trio |
|
77 | trio | |
78 | all = |
|
78 | all = |
General Comments 0
You need to be logged in to leave comments.
Login now