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