doctest.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734
  1. """Discover and run doctests in modules and test files."""
  2. import bdb
  3. import inspect
  4. import os
  5. import platform
  6. import sys
  7. import traceback
  8. import types
  9. import warnings
  10. from contextlib import contextmanager
  11. from pathlib import Path
  12. from typing import Any
  13. from typing import Callable
  14. from typing import Dict
  15. from typing import Generator
  16. from typing import Iterable
  17. from typing import List
  18. from typing import Optional
  19. from typing import Pattern
  20. from typing import Sequence
  21. from typing import Tuple
  22. from typing import Type
  23. from typing import TYPE_CHECKING
  24. from typing import Union
  25. import pytest
  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 FixtureRequest
  35. from _pytest.nodes import Collector
  36. from _pytest.outcomes import OutcomeException
  37. from _pytest.pathlib import fnmatch_ex
  38. from _pytest.pathlib import import_path
  39. from _pytest.python_api import approx
  40. from _pytest.warning_types import PytestWarning
  41. if TYPE_CHECKING:
  42. import doctest
  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: Optional[Type["doctest.OutputChecker"]] = 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 ImportErrors",
  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. ) -> Optional[Union["DoctestModule", "DoctestTextfile"]]:
  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. mod: DoctestModule = DoctestModule.from_parent(parent, path=file_path)
  120. return mod
  121. elif _is_doctest(config, file_path, parent):
  122. txt: DoctestTextfile = DoctestTextfile.from_parent(parent, path=file_path)
  123. return txt
  124. return None
  125. def _is_setup_py(path: Path) -> bool:
  126. if path.name != "setup.py":
  127. return False
  128. contents = path.read_bytes()
  129. return b"setuptools" in contents or b"distutils" in contents
  130. def _is_doctest(config: Config, path: Path, parent: Collector) -> bool:
  131. if path.suffix in (".txt", ".rst") and parent.session.isinitpath(path):
  132. return True
  133. globs = config.getoption("doctestglob") or ["test*.txt"]
  134. return any(fnmatch_ex(glob, path) for glob in globs)
  135. def _is_main_py(path: Path) -> bool:
  136. return path.name == "__main__.py"
  137. class ReprFailDoctest(TerminalRepr):
  138. def __init__(
  139. self, reprlocation_lines: Sequence[Tuple[ReprFileLocation, Sequence[str]]]
  140. ) -> None:
  141. self.reprlocation_lines = reprlocation_lines
  142. def toterminal(self, tw: TerminalWriter) -> None:
  143. for reprlocation, lines in self.reprlocation_lines:
  144. for line in lines:
  145. tw.line(line)
  146. reprlocation.toterminal(tw)
  147. class MultipleDoctestFailures(Exception):
  148. def __init__(self, failures: Sequence["doctest.DocTestFailure"]) -> None:
  149. super().__init__()
  150. self.failures = failures
  151. def _init_runner_class() -> Type["doctest.DocTestRunner"]:
  152. import doctest
  153. class PytestDoctestRunner(doctest.DebugRunner):
  154. """Runner to collect failures.
  155. Note that the out variable in this case is a list instead of a
  156. stdout-like object.
  157. """
  158. def __init__(
  159. self,
  160. checker: Optional["doctest.OutputChecker"] = None,
  161. verbose: Optional[bool] = None,
  162. optionflags: int = 0,
  163. continue_on_failure: bool = True,
  164. ) -> None:
  165. super().__init__(checker=checker, verbose=verbose, optionflags=optionflags)
  166. self.continue_on_failure = continue_on_failure
  167. def report_failure(
  168. self,
  169. out,
  170. test: "doctest.DocTest",
  171. example: "doctest.Example",
  172. got: str,
  173. ) -> None:
  174. failure = doctest.DocTestFailure(test, example, got)
  175. if self.continue_on_failure:
  176. out.append(failure)
  177. else:
  178. raise failure
  179. def report_unexpected_exception(
  180. self,
  181. out,
  182. test: "doctest.DocTest",
  183. example: "doctest.Example",
  184. exc_info: Tuple[Type[BaseException], BaseException, types.TracebackType],
  185. ) -> None:
  186. if isinstance(exc_info[1], OutcomeException):
  187. raise exc_info[1]
  188. if isinstance(exc_info[1], bdb.BdbQuit):
  189. outcomes.exit("Quitting debugger")
  190. failure = doctest.UnexpectedException(test, example, exc_info)
  191. if self.continue_on_failure:
  192. out.append(failure)
  193. else:
  194. raise failure
  195. return PytestDoctestRunner
  196. def _get_runner(
  197. checker: Optional["doctest.OutputChecker"] = None,
  198. verbose: Optional[bool] = None,
  199. optionflags: int = 0,
  200. continue_on_failure: bool = True,
  201. ) -> "doctest.DocTestRunner":
  202. # We need this in order to do a lazy import on doctest
  203. global RUNNER_CLASS
  204. if RUNNER_CLASS is None:
  205. RUNNER_CLASS = _init_runner_class()
  206. # Type ignored because the continue_on_failure argument is only defined on
  207. # PytestDoctestRunner, which is lazily defined so can't be used as a type.
  208. return RUNNER_CLASS( # type: ignore
  209. checker=checker,
  210. verbose=verbose,
  211. optionflags=optionflags,
  212. continue_on_failure=continue_on_failure,
  213. )
  214. class DoctestItem(pytest.Item):
  215. def __init__(
  216. self,
  217. name: str,
  218. parent: "Union[DoctestTextfile, DoctestModule]",
  219. runner: Optional["doctest.DocTestRunner"] = None,
  220. dtest: Optional["doctest.DocTest"] = None,
  221. ) -> None:
  222. super().__init__(name, parent)
  223. self.runner = runner
  224. self.dtest = dtest
  225. self.obj = None
  226. self.fixture_request: Optional[FixtureRequest] = None
  227. @classmethod
  228. def from_parent( # type: ignore
  229. cls,
  230. parent: "Union[DoctestTextfile, DoctestModule]",
  231. *,
  232. name: str,
  233. runner: "doctest.DocTestRunner",
  234. dtest: "doctest.DocTest",
  235. ):
  236. # incompatible signature due to imposed limits on subclass
  237. """The public named constructor."""
  238. return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
  239. def setup(self) -> None:
  240. if self.dtest is not None:
  241. self.fixture_request = _setup_fixtures(self)
  242. globs = dict(getfixture=self.fixture_request.getfixturevalue)
  243. for name, value in self.fixture_request.getfixturevalue(
  244. "doctest_namespace"
  245. ).items():
  246. globs[name] = value
  247. self.dtest.globs.update(globs)
  248. def runtest(self) -> None:
  249. assert self.dtest is not None
  250. assert self.runner is not None
  251. _check_all_skipped(self.dtest)
  252. self._disable_output_capturing_for_darwin()
  253. failures: List["doctest.DocTestFailure"] = []
  254. # Type ignored because we change the type of `out` from what
  255. # doctest expects.
  256. self.runner.run(self.dtest, out=failures) # type: ignore[arg-type]
  257. if failures:
  258. raise MultipleDoctestFailures(failures)
  259. def _disable_output_capturing_for_darwin(self) -> None:
  260. """Disable output capturing. Otherwise, stdout is lost to doctest (#985)."""
  261. if platform.system() != "Darwin":
  262. return
  263. capman = self.config.pluginmanager.getplugin("capturemanager")
  264. if capman:
  265. capman.suspend_global_capture(in_=True)
  266. out, err = capman.read_global_capture()
  267. sys.stdout.write(out)
  268. sys.stderr.write(err)
  269. # TODO: Type ignored -- breaks Liskov Substitution.
  270. def repr_failure( # type: ignore[override]
  271. self,
  272. excinfo: ExceptionInfo[BaseException],
  273. ) -> Union[str, TerminalRepr]:
  274. import doctest
  275. failures: Optional[
  276. Sequence[Union[doctest.DocTestFailure, doctest.UnexpectedException]]
  277. ] = None
  278. if isinstance(
  279. excinfo.value, (doctest.DocTestFailure, doctest.UnexpectedException)
  280. ):
  281. failures = [excinfo.value]
  282. elif isinstance(excinfo.value, MultipleDoctestFailures):
  283. failures = excinfo.value.failures
  284. if failures is None:
  285. return super().repr_failure(excinfo)
  286. reprlocation_lines = []
  287. for failure in failures:
  288. example = failure.example
  289. test = failure.test
  290. filename = test.filename
  291. if test.lineno is None:
  292. lineno = None
  293. else:
  294. lineno = test.lineno + example.lineno + 1
  295. message = type(failure).__name__
  296. # TODO: ReprFileLocation doesn't expect a None lineno.
  297. reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type]
  298. checker = _get_checker()
  299. report_choice = _get_report_choice(self.config.getoption("doctestreport"))
  300. if lineno is not None:
  301. assert failure.test.docstring is not None
  302. lines = failure.test.docstring.splitlines(False)
  303. # add line numbers to the left of the error message
  304. assert test.lineno is not None
  305. lines = [
  306. "%03d %s" % (i + test.lineno + 1, x) for (i, x) in enumerate(lines)
  307. ]
  308. # trim docstring error lines to 10
  309. lines = lines[max(example.lineno - 9, 0) : example.lineno + 1]
  310. else:
  311. lines = [
  312. "EXAMPLE LOCATION UNKNOWN, not showing all tests of that example"
  313. ]
  314. indent = ">>>"
  315. for line in example.source.splitlines():
  316. lines.append(f"??? {indent} {line}")
  317. indent = "..."
  318. if isinstance(failure, doctest.DocTestFailure):
  319. lines += checker.output_difference(
  320. example, failure.got, report_choice
  321. ).split("\n")
  322. else:
  323. inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info)
  324. lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)]
  325. lines += [
  326. x.strip("\n") for x in traceback.format_exception(*failure.exc_info)
  327. ]
  328. reprlocation_lines.append((reprlocation, lines))
  329. return ReprFailDoctest(reprlocation_lines)
  330. def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
  331. assert self.dtest is not None
  332. return self.path, self.dtest.lineno, "[doctest] %s" % 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(parent):
  347. optionflags_str = parent.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):
  354. continue_on_failure = 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(pytest.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)
  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. pytest.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, None, 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: Optional[Callable[[Any], Any]] = 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. "Got %r when unwrapping %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" % (e, func),
  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(pytest.Module):
  426. def collect(self) -> Iterable[DoctestItem]:
  427. import doctest
  428. class MockAwareDocTestFinder(doctest.DocTestFinder):
  429. """A hackish doctest finder that overrides stdlib internals to fix a stdlib bug.
  430. https://github.com/pytest-dev/pytest/issues/3456
  431. https://bugs.python.org/issue25532
  432. """
  433. def _find_lineno(self, obj, source_lines):
  434. """Doctest code does not take into account `@property`, this
  435. is a hackish way to fix it. https://bugs.python.org/issue17446
  436. Wrapped Doctests will need to be unwrapped so the correct
  437. line number is returned. This will be reported upstream. #8796
  438. """
  439. if isinstance(obj, property):
  440. obj = getattr(obj, "fget", obj)
  441. if hasattr(obj, "__wrapped__"):
  442. # Get the main obj in case of it being wrapped
  443. obj = inspect.unwrap(obj)
  444. # Type ignored because this is a private function.
  445. return super()._find_lineno( # type:ignore[misc]
  446. obj,
  447. source_lines,
  448. )
  449. def _find(
  450. self, tests, obj, name, module, source_lines, globs, seen
  451. ) -> None:
  452. if _is_mocked(obj):
  453. return
  454. with _patch_unwrap_mock_aware():
  455. # Type ignored because this is a private function.
  456. super()._find( # type:ignore[misc]
  457. tests, obj, name, module, source_lines, globs, seen
  458. )
  459. if self.path.name == "conftest.py":
  460. module = self.config.pluginmanager._importconftest(
  461. self.path,
  462. self.config.getoption("importmode"),
  463. rootpath=self.config.rootpath,
  464. )
  465. else:
  466. try:
  467. module = import_path(self.path, root=self.config.rootpath)
  468. except ImportError:
  469. if self.config.getvalue("doctest_ignore_import_errors"):
  470. pytest.skip("unable to import module %r" % self.path)
  471. else:
  472. raise
  473. # Uses internal doctest module parsing mechanism.
  474. finder = MockAwareDocTestFinder()
  475. optionflags = get_optionflags(self)
  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 _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:
  488. """Used by DoctestTextfile and DoctestItem to setup fixture information."""
  489. def func() -> None:
  490. pass
  491. doctest_item.funcargs = {} # type: ignore[attr-defined]
  492. fm = doctest_item.session._fixturemanager
  493. doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
  494. node=doctest_item, func=func, cls=None, funcargs=False
  495. )
  496. fixture_request = FixtureRequest(doctest_item, _ispytest=True)
  497. fixture_request._fillfixtures()
  498. return fixture_request
  499. def _init_checker_class() -> Type["doctest.OutputChecker"]:
  500. import doctest
  501. import re
  502. class LiteralsOutputChecker(doctest.OutputChecker):
  503. # Based on doctest_nose_plugin.py from the nltk project
  504. # (https://github.com/nltk/nltk) and on the "numtest" doctest extension
  505. # by Sebastien Boisgerault (https://github.com/boisgera/numtest).
  506. _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
  507. _bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE)
  508. _number_re = re.compile(
  509. r"""
  510. (?P<number>
  511. (?P<mantissa>
  512. (?P<integer1> [+-]?\d*)\.(?P<fraction>\d+)
  513. |
  514. (?P<integer2> [+-]?\d+)\.
  515. )
  516. (?:
  517. [Ee]
  518. (?P<exponent1> [+-]?\d+)
  519. )?
  520. |
  521. (?P<integer3> [+-]?\d+)
  522. (?:
  523. [Ee]
  524. (?P<exponent2> [+-]?\d+)
  525. )
  526. )
  527. """,
  528. re.VERBOSE,
  529. )
  530. def check_output(self, want: str, got: str, optionflags: int) -> bool:
  531. if super().check_output(want, got, optionflags):
  532. return True
  533. allow_unicode = optionflags & _get_allow_unicode_flag()
  534. allow_bytes = optionflags & _get_allow_bytes_flag()
  535. allow_number = optionflags & _get_number_flag()
  536. if not allow_unicode and not allow_bytes and not allow_number:
  537. return False
  538. def remove_prefixes(regex: Pattern[str], txt: str) -> str:
  539. return re.sub(regex, r"\1\2", txt)
  540. if allow_unicode:
  541. want = remove_prefixes(self._unicode_literal_re, want)
  542. got = remove_prefixes(self._unicode_literal_re, got)
  543. if allow_bytes:
  544. want = remove_prefixes(self._bytes_literal_re, want)
  545. got = remove_prefixes(self._bytes_literal_re, got)
  546. if allow_number:
  547. got = self._remove_unwanted_precision(want, got)
  548. return super().check_output(want, got, optionflags)
  549. def _remove_unwanted_precision(self, want: str, got: str) -> str:
  550. wants = list(self._number_re.finditer(want))
  551. gots = list(self._number_re.finditer(got))
  552. if len(wants) != len(gots):
  553. return got
  554. offset = 0
  555. for w, g in zip(wants, gots):
  556. fraction: Optional[str] = w.group("fraction")
  557. exponent: Optional[str] = w.group("exponent1")
  558. if exponent is None:
  559. exponent = w.group("exponent2")
  560. precision = 0 if fraction is None else len(fraction)
  561. if exponent is not None:
  562. precision -= int(exponent)
  563. if float(w.group()) == approx(float(g.group()), abs=10 ** -precision):
  564. # They're close enough. Replace the text we actually
  565. # got with the text we want, so that it will match when we
  566. # check the string literally.
  567. got = (
  568. got[: g.start() + offset] + w.group() + got[g.end() + offset :]
  569. )
  570. offset += w.end() - w.start() - (g.end() - g.start())
  571. return got
  572. return LiteralsOutputChecker
  573. def _get_checker() -> "doctest.OutputChecker":
  574. """Return a doctest.OutputChecker subclass that supports some
  575. additional options:
  576. * ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b''
  577. prefixes (respectively) in string literals. Useful when the same
  578. doctest should run in Python 2 and Python 3.
  579. * NUMBER to ignore floating-point differences smaller than the
  580. precision of the literal number in the doctest.
  581. An inner class is used to avoid importing "doctest" at the module
  582. level.
  583. """
  584. global CHECKER_CLASS
  585. if CHECKER_CLASS is None:
  586. CHECKER_CLASS = _init_checker_class()
  587. return CHECKER_CLASS()
  588. def _get_allow_unicode_flag() -> int:
  589. """Register and return the ALLOW_UNICODE flag."""
  590. import doctest
  591. return doctest.register_optionflag("ALLOW_UNICODE")
  592. def _get_allow_bytes_flag() -> int:
  593. """Register and return the ALLOW_BYTES flag."""
  594. import doctest
  595. return doctest.register_optionflag("ALLOW_BYTES")
  596. def _get_number_flag() -> int:
  597. """Register and return the NUMBER flag."""
  598. import doctest
  599. return doctest.register_optionflag("NUMBER")
  600. def _get_report_choice(key: str) -> int:
  601. """Return the actual `doctest` module flag value.
  602. We want to do it as late as possible to avoid importing `doctest` and all
  603. its dependencies when parsing options, as it adds overhead and breaks tests.
  604. """
  605. import doctest
  606. return {
  607. DOCTEST_REPORT_CHOICE_UDIFF: doctest.REPORT_UDIFF,
  608. DOCTEST_REPORT_CHOICE_CDIFF: doctest.REPORT_CDIFF,
  609. DOCTEST_REPORT_CHOICE_NDIFF: doctest.REPORT_NDIFF,
  610. DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE: doctest.REPORT_ONLY_FIRST_FAILURE,
  611. DOCTEST_REPORT_CHOICE_NONE: 0,
  612. }[key]
  613. @pytest.fixture(scope="session")
  614. def doctest_namespace() -> Dict[str, Any]:
  615. """Fixture that returns a :py:class:`dict` that will be injected into the
  616. namespace of doctests."""
  617. return dict()