You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

725 lines
25KB

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