doctest.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771
  1. """Discover and run doctests in modules and test files."""
  2. import bdb
  3. import functools
  4. import inspect
  5. import os
  6. import platform
  7. import sys
  8. import traceback
  9. import types
  10. import warnings
  11. from contextlib import contextmanager
  12. from pathlib import Path
  13. from typing import Any
  14. from typing import Callable
  15. from typing import Dict
  16. from typing import Generator
  17. from typing import Iterable
  18. from typing import List
  19. from typing import Optional
  20. from typing import Pattern
  21. from typing import Sequence
  22. from typing import Tuple
  23. from typing import Type
  24. from typing import TYPE_CHECKING
  25. from typing import Union
  26. from _pytest import outcomes
  27. from _pytest._code.code import ExceptionInfo
  28. from _pytest._code.code import ReprFileLocation
  29. from _pytest._code.code import TerminalRepr
  30. from _pytest._io import TerminalWriter
  31. from _pytest.compat import safe_getattr
  32. from _pytest.config import Config
  33. from _pytest.config.argparsing import Parser
  34. from _pytest.fixtures import fixture
  35. from _pytest.fixtures import FixtureRequest
  36. from _pytest.nodes import Collector
  37. from _pytest.nodes import Item
  38. from _pytest.outcomes import OutcomeException
  39. from _pytest.outcomes import skip
  40. from _pytest.pathlib import fnmatch_ex
  41. from _pytest.pathlib import import_path
  42. from _pytest.python import Module
  43. from _pytest.python_api import approx
  44. from _pytest.warning_types import PytestWarning
  45. if TYPE_CHECKING:
  46. import doctest
  47. DOCTEST_REPORT_CHOICE_NONE = "none"
  48. DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
  49. DOCTEST_REPORT_CHOICE_NDIFF = "ndiff"
  50. DOCTEST_REPORT_CHOICE_UDIFF = "udiff"
  51. DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE = "only_first_failure"
  52. DOCTEST_REPORT_CHOICES = (
  53. DOCTEST_REPORT_CHOICE_NONE,
  54. DOCTEST_REPORT_CHOICE_CDIFF,
  55. DOCTEST_REPORT_CHOICE_NDIFF,
  56. DOCTEST_REPORT_CHOICE_UDIFF,
  57. DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE,
  58. )
  59. # Lazy definition of runner class
  60. RUNNER_CLASS = None
  61. # Lazy definition of output checker class
  62. CHECKER_CLASS: Optional[Type["doctest.OutputChecker"]] = None
  63. def pytest_addoption(parser: Parser) -> None:
  64. parser.addini(
  65. "doctest_optionflags",
  66. "Option flags for doctests",
  67. type="args",
  68. default=["ELLIPSIS"],
  69. )
  70. parser.addini(
  71. "doctest_encoding", "Encoding used for doctest files", default="utf-8"
  72. )
  73. group = parser.getgroup("collect")
  74. group.addoption(
  75. "--doctest-modules",
  76. action="store_true",
  77. default=False,
  78. help="Run doctests in all .py modules",
  79. dest="doctestmodules",
  80. )
  81. group.addoption(
  82. "--doctest-report",
  83. type=str.lower,
  84. default="udiff",
  85. help="Choose another output format for diffs on doctest failure",
  86. choices=DOCTEST_REPORT_CHOICES,
  87. dest="doctestreport",
  88. )
  89. group.addoption(
  90. "--doctest-glob",
  91. action="append",
  92. default=[],
  93. metavar="pat",
  94. help="Doctests file matching pattern, default: test*.txt",
  95. dest="doctestglob",
  96. )
  97. group.addoption(
  98. "--doctest-ignore-import-errors",
  99. action="store_true",
  100. default=False,
  101. help="Ignore doctest ImportErrors",
  102. dest="doctest_ignore_import_errors",
  103. )
  104. group.addoption(
  105. "--doctest-continue-on-failure",
  106. action="store_true",
  107. default=False,
  108. help="For a given doctest, continue to run after the first failure",
  109. dest="doctest_continue_on_failure",
  110. )
  111. def pytest_unconfigure() -> None:
  112. global RUNNER_CLASS
  113. RUNNER_CLASS = None
  114. def pytest_collect_file(
  115. file_path: Path,
  116. parent: Collector,
  117. ) -> Optional[Union["DoctestModule", "DoctestTextfile"]]:
  118. config = parent.config
  119. if file_path.suffix == ".py":
  120. if config.option.doctestmodules and not any(
  121. (_is_setup_py(file_path), _is_main_py(file_path))
  122. ):
  123. mod: DoctestModule = DoctestModule.from_parent(parent, path=file_path)
  124. return mod
  125. elif _is_doctest(config, file_path, parent):
  126. txt: DoctestTextfile = DoctestTextfile.from_parent(parent, path=file_path)
  127. return txt
  128. return None
  129. def _is_setup_py(path: Path) -> bool:
  130. if path.name != "setup.py":
  131. return False
  132. contents = path.read_bytes()
  133. return b"setuptools" in contents or b"distutils" in contents
  134. def _is_doctest(config: Config, path: Path, parent: Collector) -> bool:
  135. if path.suffix in (".txt", ".rst") and parent.session.isinitpath(path):
  136. return True
  137. globs = config.getoption("doctestglob") or ["test*.txt"]
  138. return any(fnmatch_ex(glob, path) for glob in globs)
  139. def _is_main_py(path: Path) -> bool:
  140. return path.name == "__main__.py"
  141. class ReprFailDoctest(TerminalRepr):
  142. def __init__(
  143. self, reprlocation_lines: Sequence[Tuple[ReprFileLocation, Sequence[str]]]
  144. ) -> None:
  145. self.reprlocation_lines = reprlocation_lines
  146. def toterminal(self, tw: TerminalWriter) -> None:
  147. for reprlocation, lines in self.reprlocation_lines:
  148. for line in lines:
  149. tw.line(line)
  150. reprlocation.toterminal(tw)
  151. class MultipleDoctestFailures(Exception):
  152. def __init__(self, failures: Sequence["doctest.DocTestFailure"]) -> None:
  153. super().__init__()
  154. self.failures = failures
  155. def _init_runner_class() -> Type["doctest.DocTestRunner"]:
  156. import doctest
  157. class PytestDoctestRunner(doctest.DebugRunner):
  158. """Runner to collect failures.
  159. Note that the out variable in this case is a list instead of a
  160. stdout-like object.
  161. """
  162. def __init__(
  163. self,
  164. checker: Optional["doctest.OutputChecker"] = None,
  165. verbose: Optional[bool] = None,
  166. optionflags: int = 0,
  167. continue_on_failure: bool = True,
  168. ) -> None:
  169. super().__init__(checker=checker, verbose=verbose, optionflags=optionflags)
  170. self.continue_on_failure = continue_on_failure
  171. def report_failure(
  172. self,
  173. out,
  174. test: "doctest.DocTest",
  175. example: "doctest.Example",
  176. got: str,
  177. ) -> None:
  178. failure = doctest.DocTestFailure(test, example, got)
  179. if self.continue_on_failure:
  180. out.append(failure)
  181. else:
  182. raise failure
  183. def report_unexpected_exception(
  184. self,
  185. out,
  186. test: "doctest.DocTest",
  187. example: "doctest.Example",
  188. exc_info: Tuple[Type[BaseException], BaseException, types.TracebackType],
  189. ) -> None:
  190. if isinstance(exc_info[1], OutcomeException):
  191. raise exc_info[1]
  192. if isinstance(exc_info[1], bdb.BdbQuit):
  193. outcomes.exit("Quitting debugger")
  194. failure = doctest.UnexpectedException(test, example, exc_info)
  195. if self.continue_on_failure:
  196. out.append(failure)
  197. else:
  198. raise failure
  199. return PytestDoctestRunner
  200. def _get_runner(
  201. checker: Optional["doctest.OutputChecker"] = None,
  202. verbose: Optional[bool] = None,
  203. optionflags: int = 0,
  204. continue_on_failure: bool = True,
  205. ) -> "doctest.DocTestRunner":
  206. # We need this in order to do a lazy import on doctest
  207. global RUNNER_CLASS
  208. if RUNNER_CLASS is None:
  209. RUNNER_CLASS = _init_runner_class()
  210. # Type ignored because the continue_on_failure argument is only defined on
  211. # PytestDoctestRunner, which is lazily defined so can't be used as a type.
  212. return RUNNER_CLASS( # type: ignore
  213. checker=checker,
  214. verbose=verbose,
  215. optionflags=optionflags,
  216. continue_on_failure=continue_on_failure,
  217. )
  218. class DoctestItem(Item):
  219. def __init__(
  220. self,
  221. name: str,
  222. parent: "Union[DoctestTextfile, DoctestModule]",
  223. runner: Optional["doctest.DocTestRunner"] = None,
  224. dtest: Optional["doctest.DocTest"] = None,
  225. ) -> None:
  226. super().__init__(name, parent)
  227. self.runner = runner
  228. self.dtest = dtest
  229. self.obj = None
  230. self.fixture_request: Optional[FixtureRequest] = None
  231. @classmethod
  232. def from_parent( # type: ignore
  233. cls,
  234. parent: "Union[DoctestTextfile, DoctestModule]",
  235. *,
  236. name: str,
  237. runner: "doctest.DocTestRunner",
  238. dtest: "doctest.DocTest",
  239. ):
  240. # incompatible signature due to imposed limits on subclass
  241. """The public named constructor."""
  242. return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
  243. def setup(self) -> None:
  244. if self.dtest is not None:
  245. self.fixture_request = _setup_fixtures(self)
  246. globs = dict(getfixture=self.fixture_request.getfixturevalue)
  247. for name, value in self.fixture_request.getfixturevalue(
  248. "doctest_namespace"
  249. ).items():
  250. globs[name] = value
  251. self.dtest.globs.update(globs)
  252. def runtest(self) -> None:
  253. assert self.dtest is not None
  254. assert self.runner is not None
  255. _check_all_skipped(self.dtest)
  256. self._disable_output_capturing_for_darwin()
  257. failures: List["doctest.DocTestFailure"] = []
  258. # Type ignored because we change the type of `out` from what
  259. # doctest expects.
  260. self.runner.run(self.dtest, out=failures) # type: ignore[arg-type]
  261. if failures:
  262. raise MultipleDoctestFailures(failures)
  263. def _disable_output_capturing_for_darwin(self) -> None:
  264. """Disable output capturing. Otherwise, stdout is lost to doctest (#985)."""
  265. if platform.system() != "Darwin":
  266. return
  267. capman = self.config.pluginmanager.getplugin("capturemanager")
  268. if capman:
  269. capman.suspend_global_capture(in_=True)
  270. out, err = capman.read_global_capture()
  271. sys.stdout.write(out)
  272. sys.stderr.write(err)
  273. # TODO: Type ignored -- breaks Liskov Substitution.
  274. def repr_failure( # type: ignore[override]
  275. self,
  276. excinfo: ExceptionInfo[BaseException],
  277. ) -> Union[str, TerminalRepr]:
  278. import doctest
  279. failures: Optional[
  280. Sequence[Union[doctest.DocTestFailure, doctest.UnexpectedException]]
  281. ] = None
  282. if isinstance(
  283. excinfo.value, (doctest.DocTestFailure, doctest.UnexpectedException)
  284. ):
  285. failures = [excinfo.value]
  286. elif isinstance(excinfo.value, MultipleDoctestFailures):
  287. failures = excinfo.value.failures
  288. if failures is None:
  289. return super().repr_failure(excinfo)
  290. reprlocation_lines = []
  291. for failure in failures:
  292. example = failure.example
  293. test = failure.test
  294. filename = test.filename
  295. if test.lineno is None:
  296. lineno = None
  297. else:
  298. lineno = test.lineno + example.lineno + 1
  299. message = type(failure).__name__
  300. # TODO: ReprFileLocation doesn't expect a None lineno.
  301. reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type]
  302. checker = _get_checker()
  303. report_choice = _get_report_choice(self.config.getoption("doctestreport"))
  304. if lineno is not None:
  305. assert failure.test.docstring is not None
  306. lines = failure.test.docstring.splitlines(False)
  307. # add line numbers to the left of the error message
  308. assert test.lineno is not None
  309. lines = [
  310. "%03d %s" % (i + test.lineno + 1, x) for (i, x) in enumerate(lines)
  311. ]
  312. # trim docstring error lines to 10
  313. lines = lines[max(example.lineno - 9, 0) : example.lineno + 1]
  314. else:
  315. lines = [
  316. "EXAMPLE LOCATION UNKNOWN, not showing all tests of that example"
  317. ]
  318. indent = ">>>"
  319. for line in example.source.splitlines():
  320. lines.append(f"??? {indent} {line}")
  321. indent = "..."
  322. if isinstance(failure, doctest.DocTestFailure):
  323. lines += checker.output_difference(
  324. example, failure.got, report_choice
  325. ).split("\n")
  326. else:
  327. inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info)
  328. lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)]
  329. lines += [
  330. x.strip("\n") for x in traceback.format_exception(*failure.exc_info)
  331. ]
  332. reprlocation_lines.append((reprlocation, lines))
  333. return ReprFailDoctest(reprlocation_lines)
  334. def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
  335. assert self.dtest is not None
  336. return self.path, self.dtest.lineno, "[doctest] %s" % self.name
  337. def _get_flag_lookup() -> Dict[str, int]:
  338. import doctest
  339. return dict(
  340. DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1,
  341. DONT_ACCEPT_BLANKLINE=doctest.DONT_ACCEPT_BLANKLINE,
  342. NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE,
  343. ELLIPSIS=doctest.ELLIPSIS,
  344. IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL,
  345. COMPARISON_FLAGS=doctest.COMPARISON_FLAGS,
  346. ALLOW_UNICODE=_get_allow_unicode_flag(),
  347. ALLOW_BYTES=_get_allow_bytes_flag(),
  348. NUMBER=_get_number_flag(),
  349. )
  350. def get_optionflags(parent):
  351. optionflags_str = parent.config.getini("doctest_optionflags")
  352. flag_lookup_table = _get_flag_lookup()
  353. flag_acc = 0
  354. for flag in optionflags_str:
  355. flag_acc |= flag_lookup_table[flag]
  356. return flag_acc
  357. def _get_continue_on_failure(config):
  358. continue_on_failure = config.getvalue("doctest_continue_on_failure")
  359. if continue_on_failure:
  360. # We need to turn off this if we use pdb since we should stop at
  361. # the first failure.
  362. if config.getvalue("usepdb"):
  363. continue_on_failure = False
  364. return continue_on_failure
  365. class DoctestTextfile(Module):
  366. obj = None
  367. def collect(self) -> Iterable[DoctestItem]:
  368. import doctest
  369. # Inspired by doctest.testfile; ideally we would use it directly,
  370. # but it doesn't support passing a custom checker.
  371. encoding = self.config.getini("doctest_encoding")
  372. text = self.path.read_text(encoding)
  373. filename = str(self.path)
  374. name = self.path.name
  375. globs = {"__name__": "__main__"}
  376. optionflags = get_optionflags(self)
  377. runner = _get_runner(
  378. verbose=False,
  379. optionflags=optionflags,
  380. checker=_get_checker(),
  381. continue_on_failure=_get_continue_on_failure(self.config),
  382. )
  383. parser = doctest.DocTestParser()
  384. test = parser.get_doctest(text, globs, name, filename, 0)
  385. if test.examples:
  386. yield DoctestItem.from_parent(
  387. self, name=test.name, runner=runner, dtest=test
  388. )
  389. def _check_all_skipped(test: "doctest.DocTest") -> None:
  390. """Raise pytest.skip() if all examples in the given DocTest have the SKIP
  391. option set."""
  392. import doctest
  393. all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples)
  394. if all_skipped:
  395. skip("all tests skipped by +SKIP option")
  396. def _is_mocked(obj: object) -> bool:
  397. """Return if an object is possibly a mock object by checking the
  398. existence of a highly improbable attribute."""
  399. return (
  400. safe_getattr(obj, "pytest_mock_example_attribute_that_shouldnt_exist", None)
  401. is not None
  402. )
  403. @contextmanager
  404. def _patch_unwrap_mock_aware() -> Generator[None, None, None]:
  405. """Context manager which replaces ``inspect.unwrap`` with a version
  406. that's aware of mock objects and doesn't recurse into them."""
  407. real_unwrap = inspect.unwrap
  408. def _mock_aware_unwrap(
  409. func: Callable[..., Any], *, stop: Optional[Callable[[Any], Any]] = None
  410. ) -> Any:
  411. try:
  412. if stop is None or stop is _is_mocked:
  413. return real_unwrap(func, stop=_is_mocked)
  414. _stop = stop
  415. return real_unwrap(func, stop=lambda obj: _is_mocked(obj) or _stop(func))
  416. except Exception as e:
  417. warnings.warn(
  418. "Got %r when unwrapping %r. This is usually caused "
  419. "by a violation of Python's object protocol; see e.g. "
  420. "https://github.com/pytest-dev/pytest/issues/5080" % (e, func),
  421. PytestWarning,
  422. )
  423. raise
  424. inspect.unwrap = _mock_aware_unwrap
  425. try:
  426. yield
  427. finally:
  428. inspect.unwrap = real_unwrap
  429. class DoctestModule(Module):
  430. def collect(self) -> Iterable[DoctestItem]:
  431. import doctest
  432. class MockAwareDocTestFinder(doctest.DocTestFinder):
  433. """A hackish doctest finder that overrides stdlib internals to fix a stdlib bug.
  434. https://github.com/pytest-dev/pytest/issues/3456
  435. https://bugs.python.org/issue25532
  436. """
  437. def _find_lineno(self, obj, source_lines):
  438. """Doctest code does not take into account `@property`, this
  439. is a hackish way to fix it. https://bugs.python.org/issue17446
  440. Wrapped Doctests will need to be unwrapped so the correct
  441. line number is returned. This will be reported upstream. #8796
  442. """
  443. if isinstance(obj, property):
  444. obj = getattr(obj, "fget", obj)
  445. if hasattr(obj, "__wrapped__"):
  446. # Get the main obj in case of it being wrapped
  447. obj = inspect.unwrap(obj)
  448. # Type ignored because this is a private function.
  449. return super()._find_lineno( # type:ignore[misc]
  450. obj,
  451. source_lines,
  452. )
  453. def _find(
  454. self, tests, obj, name, module, source_lines, globs, seen
  455. ) -> None:
  456. if _is_mocked(obj):
  457. return
  458. with _patch_unwrap_mock_aware():
  459. # Type ignored because this is a private function.
  460. super()._find( # type:ignore[misc]
  461. tests, obj, name, module, source_lines, globs, seen
  462. )
  463. if sys.version_info < (3, 13):
  464. def _from_module(self, module, object):
  465. """`cached_property` objects are never considered a part
  466. of the 'current module'. As such they are skipped by doctest.
  467. Here we override `_from_module` to check the underlying
  468. function instead. https://github.com/python/cpython/issues/107995
  469. """
  470. if hasattr(functools, "cached_property") and isinstance(
  471. object, functools.cached_property
  472. ):
  473. object = object.func
  474. # Type ignored because this is a private function.
  475. return super()._from_module(module, object) # type: ignore[misc]
  476. else: # pragma: no cover
  477. pass
  478. if self.path.name == "conftest.py":
  479. module = self.config.pluginmanager._importconftest(
  480. self.path,
  481. self.config.getoption("importmode"),
  482. rootpath=self.config.rootpath,
  483. )
  484. else:
  485. try:
  486. module = import_path(
  487. self.path,
  488. root=self.config.rootpath,
  489. mode=self.config.getoption("importmode"),
  490. )
  491. except ImportError:
  492. if self.config.getvalue("doctest_ignore_import_errors"):
  493. skip("unable to import module %r" % self.path)
  494. else:
  495. raise
  496. # Uses internal doctest module parsing mechanism.
  497. finder = MockAwareDocTestFinder()
  498. optionflags = get_optionflags(self)
  499. runner = _get_runner(
  500. verbose=False,
  501. optionflags=optionflags,
  502. checker=_get_checker(),
  503. continue_on_failure=_get_continue_on_failure(self.config),
  504. )
  505. for test in finder.find(module, module.__name__):
  506. if test.examples: # skip empty doctests
  507. yield DoctestItem.from_parent(
  508. self, name=test.name, runner=runner, dtest=test
  509. )
  510. def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:
  511. """Used by DoctestTextfile and DoctestItem to setup fixture information."""
  512. def func() -> None:
  513. pass
  514. doctest_item.funcargs = {} # type: ignore[attr-defined]
  515. fm = doctest_item.session._fixturemanager
  516. doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
  517. node=doctest_item, func=func, cls=None, funcargs=False
  518. )
  519. fixture_request = FixtureRequest(doctest_item, _ispytest=True)
  520. fixture_request._fillfixtures()
  521. return fixture_request
  522. def _init_checker_class() -> Type["doctest.OutputChecker"]:
  523. import doctest
  524. import re
  525. class LiteralsOutputChecker(doctest.OutputChecker):
  526. # Based on doctest_nose_plugin.py from the nltk project
  527. # (https://github.com/nltk/nltk) and on the "numtest" doctest extension
  528. # by Sebastien Boisgerault (https://github.com/boisgera/numtest).
  529. _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
  530. _bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE)
  531. _number_re = re.compile(
  532. r"""
  533. (?P<number>
  534. (?P<mantissa>
  535. (?P<integer1> [+-]?\d*)\.(?P<fraction>\d+)
  536. |
  537. (?P<integer2> [+-]?\d+)\.
  538. )
  539. (?:
  540. [Ee]
  541. (?P<exponent1> [+-]?\d+)
  542. )?
  543. |
  544. (?P<integer3> [+-]?\d+)
  545. (?:
  546. [Ee]
  547. (?P<exponent2> [+-]?\d+)
  548. )
  549. )
  550. """,
  551. re.VERBOSE,
  552. )
  553. def check_output(self, want: str, got: str, optionflags: int) -> bool:
  554. if super().check_output(want, got, optionflags):
  555. return True
  556. allow_unicode = optionflags & _get_allow_unicode_flag()
  557. allow_bytes = optionflags & _get_allow_bytes_flag()
  558. allow_number = optionflags & _get_number_flag()
  559. if not allow_unicode and not allow_bytes and not allow_number:
  560. return False
  561. def remove_prefixes(regex: Pattern[str], txt: str) -> str:
  562. return re.sub(regex, r"\1\2", txt)
  563. if allow_unicode:
  564. want = remove_prefixes(self._unicode_literal_re, want)
  565. got = remove_prefixes(self._unicode_literal_re, got)
  566. if allow_bytes:
  567. want = remove_prefixes(self._bytes_literal_re, want)
  568. got = remove_prefixes(self._bytes_literal_re, got)
  569. if allow_number:
  570. got = self._remove_unwanted_precision(want, got)
  571. return super().check_output(want, got, optionflags)
  572. def _remove_unwanted_precision(self, want: str, got: str) -> str:
  573. wants = list(self._number_re.finditer(want))
  574. gots = list(self._number_re.finditer(got))
  575. if len(wants) != len(gots):
  576. return got
  577. offset = 0
  578. for w, g in zip(wants, gots):
  579. fraction: Optional[str] = w.group("fraction")
  580. exponent: Optional[str] = w.group("exponent1")
  581. if exponent is None:
  582. exponent = w.group("exponent2")
  583. precision = 0 if fraction is None else len(fraction)
  584. if exponent is not None:
  585. precision -= int(exponent)
  586. if float(w.group()) == approx(float(g.group()), abs=10**-precision):
  587. # They're close enough. Replace the text we actually
  588. # got with the text we want, so that it will match when we
  589. # check the string literally.
  590. got = (
  591. got[: g.start() + offset] + w.group() + got[g.end() + offset :]
  592. )
  593. offset += w.end() - w.start() - (g.end() - g.start())
  594. return got
  595. return LiteralsOutputChecker
  596. def _get_checker() -> "doctest.OutputChecker":
  597. """Return a doctest.OutputChecker subclass that supports some
  598. additional options:
  599. * ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b''
  600. prefixes (respectively) in string literals. Useful when the same
  601. doctest should run in Python 2 and Python 3.
  602. * NUMBER to ignore floating-point differences smaller than the
  603. precision of the literal number in the doctest.
  604. An inner class is used to avoid importing "doctest" at the module
  605. level.
  606. """
  607. global CHECKER_CLASS
  608. if CHECKER_CLASS is None:
  609. CHECKER_CLASS = _init_checker_class()
  610. return CHECKER_CLASS()
  611. def _get_allow_unicode_flag() -> int:
  612. """Register and return the ALLOW_UNICODE flag."""
  613. import doctest
  614. return doctest.register_optionflag("ALLOW_UNICODE")
  615. def _get_allow_bytes_flag() -> int:
  616. """Register and return the ALLOW_BYTES flag."""
  617. import doctest
  618. return doctest.register_optionflag("ALLOW_BYTES")
  619. def _get_number_flag() -> int:
  620. """Register and return the NUMBER flag."""
  621. import doctest
  622. return doctest.register_optionflag("NUMBER")
  623. def _get_report_choice(key: str) -> int:
  624. """Return the actual `doctest` module flag value.
  625. We want to do it as late as possible to avoid importing `doctest` and all
  626. its dependencies when parsing options, as it adds overhead and breaks tests.
  627. """
  628. import doctest
  629. return {
  630. DOCTEST_REPORT_CHOICE_UDIFF: doctest.REPORT_UDIFF,
  631. DOCTEST_REPORT_CHOICE_CDIFF: doctest.REPORT_CDIFF,
  632. DOCTEST_REPORT_CHOICE_NDIFF: doctest.REPORT_NDIFF,
  633. DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE: doctest.REPORT_ONLY_FIRST_FAILURE,
  634. DOCTEST_REPORT_CHOICE_NONE: 0,
  635. }[key]
  636. @fixture(scope="session")
  637. def doctest_namespace() -> Dict[str, Any]:
  638. """Fixture that returns a :py:class:`dict` that will be injected into the
  639. namespace of doctests.
  640. Usually this fixture is used in conjunction with another ``autouse`` fixture:
  641. .. code-block:: python
  642. @pytest.fixture(autouse=True)
  643. def add_np(doctest_namespace):
  644. doctest_namespace["np"] = numpy
  645. For more details: :ref:`doctest_namespace`.
  646. """
  647. return dict()