doctest.py 25 KB

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