##// END OF EJS Templates
ipdoctest: Merge upstream changes to pytest doctest plugin
Nikita Kniazev -
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: py.path.local,
129 file_path: Path,
130 130 parent: Collector,
131 131 ) -> Optional[Union["IPDoctestModule", "IPDoctestTextfile"]]:
132 132 config = parent.config
133 if path.ext == ".py":
134 if config.option.ipdoctestmodules and not _is_setup_py(path):
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, fspath=path)
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: py.path.local) -> bool:
144 if path.basename != "setup.py":
145 def _is_setup_py(path: Path) -> bool:
146 if path.name != "setup.py":
145 147 return False
146 contents = path.read_binary()
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: py.path.local, parent) -> bool:
151 if path.ext in (".txt", ".rst") and parent.session.isinitpath(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 to imposed limits on sublcass
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,7 +375,9 b' class IPDoctestItem(pytest.Item):'
372 375 elif isinstance(excinfo.value, MultipleDoctestFailures):
373 376 failures = excinfo.value.failures
374 377
375 if failures is not None:
378 if failures is None:
379 return super().repr_failure(excinfo)
380
376 381 reprlocation_lines = []
377 382 for failure in failures:
378 383 example = failure.example
@@ -386,17 +391,14 b' class IPDoctestItem(pytest.Item):'
386 391 # TODO: ReprFileLocation doesn't expect a None lineno.
387 392 reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type]
388 393 checker = _get_checker()
389 report_choice = _get_report_choice(
390 self.config.getoption("ipdoctestreport")
391 )
394 report_choice = _get_report_choice(self.config.getoption("ipdoctestreport"))
392 395 if lineno is not None:
393 396 assert failure.test.docstring is not None
394 397 lines = failure.test.docstring.splitlines(False)
395 398 # add line numbers to the left of the error message
396 399 assert test.lineno is not None
397 400 lines = [
398 "%03d %s" % (i + test.lineno + 1, x)
399 for (i, x) in enumerate(lines)
401 "%03d %s" % (i + test.lineno + 1, x) for (i, x) in enumerate(lines)
400 402 ]
401 403 # trim docstring error lines to 10
402 404 lines = lines[max(example.lineno - 9, 0) : example.lineno + 1]
@@ -413,20 +415,17 b' class IPDoctestItem(pytest.Item):'
413 415 example, failure.got, report_choice
414 416 ).split("\n")
415 417 else:
416 inner_excinfo = ExceptionInfo(failure.exc_info)
418 inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info)
417 419 lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)]
418 420 lines += [
419 x.strip("\n")
420 for x in traceback.format_exception(*failure.exc_info)
421 x.strip("\n") for x in traceback.format_exception(*failure.exc_info)
421 422 ]
422 423 reprlocation_lines.append((reprlocation, lines))
423 424 return ReprFailDoctest(reprlocation_lines)
424 else:
425 return super().repr_failure(excinfo)
426 425
427 def reportinfo(self):
426 def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
428 427 assert self.dtest is not None
429 return self.fspath, self.dtest.lineno, "[ipdoctest] %s" % self.name
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.fspath.read_text(encoding)
478 filename = str(self.fspath)
479 name = self.fspath.basename
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 DocTestFinder._find_lineno( # type: ignore
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 DocTestFinder._find( # type: ignore
584 self, tests, obj, name, module, source_lines, globs, seen
587 super()._find( # type:ignore[misc]
588 tests, obj, name, module, source_lines, globs, seen
585 589 )
586 590
587 if self.fspath.basename == "conftest.py":
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.fspath)
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.fspath)
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 IPDoctestOutputChecker.check_output(self, want, got, optionflags):
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 IPDoctestOutputChecker.check_output(self, want, got, optionflags)
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,10 +708,7 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 714 if float(w.group()) == approx(float(g.group()), abs=10 ** -precision):
General Comments 0
You need to be logged in to leave comments. Login now