| |
| """Discover and run doctests in modules and test files.""" |
|
|
| from __future__ import annotations |
|
|
| import bdb |
| from collections.abc import Callable |
| from collections.abc import Generator |
| from collections.abc import Iterable |
| from collections.abc import Sequence |
| from contextlib import contextmanager |
| import functools |
| import inspect |
| import os |
| from pathlib import Path |
| import platform |
| import re |
| import sys |
| import traceback |
| import types |
| from typing import Any |
| from typing import TYPE_CHECKING |
| import warnings |
|
|
| from _pytest import outcomes |
| from _pytest._code.code import ExceptionInfo |
| from _pytest._code.code import ReprFileLocation |
| from _pytest._code.code import TerminalRepr |
| from _pytest._io import TerminalWriter |
| from _pytest.compat import safe_getattr |
| from _pytest.config import Config |
| from _pytest.config.argparsing import Parser |
| from _pytest.fixtures import fixture |
| from _pytest.fixtures import TopRequest |
| from _pytest.nodes import Collector |
| from _pytest.nodes import Item |
| from _pytest.outcomes import OutcomeException |
| from _pytest.outcomes import skip |
| from _pytest.pathlib import fnmatch_ex |
| from _pytest.python import Module |
| from _pytest.python_api import approx |
| from _pytest.warning_types import PytestWarning |
|
|
|
|
| if TYPE_CHECKING: |
| import doctest |
|
|
| from typing_extensions import Self |
|
|
| DOCTEST_REPORT_CHOICE_NONE = "none" |
| DOCTEST_REPORT_CHOICE_CDIFF = "cdiff" |
| DOCTEST_REPORT_CHOICE_NDIFF = "ndiff" |
| DOCTEST_REPORT_CHOICE_UDIFF = "udiff" |
| DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE = "only_first_failure" |
|
|
| DOCTEST_REPORT_CHOICES = ( |
| DOCTEST_REPORT_CHOICE_NONE, |
| DOCTEST_REPORT_CHOICE_CDIFF, |
| DOCTEST_REPORT_CHOICE_NDIFF, |
| DOCTEST_REPORT_CHOICE_UDIFF, |
| DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE, |
| ) |
|
|
| |
| RUNNER_CLASS = None |
| |
| CHECKER_CLASS: type[doctest.OutputChecker] | None = None |
|
|
|
|
| def pytest_addoption(parser: Parser) -> None: |
| parser.addini( |
| "doctest_optionflags", |
| "Option flags for doctests", |
| type="args", |
| default=["ELLIPSIS"], |
| ) |
| parser.addini( |
| "doctest_encoding", "Encoding used for doctest files", default="utf-8" |
| ) |
| group = parser.getgroup("collect") |
| group.addoption( |
| "--doctest-modules", |
| action="store_true", |
| default=False, |
| help="Run doctests in all .py modules", |
| dest="doctestmodules", |
| ) |
| group.addoption( |
| "--doctest-report", |
| type=str.lower, |
| default="udiff", |
| help="Choose another output format for diffs on doctest failure", |
| choices=DOCTEST_REPORT_CHOICES, |
| dest="doctestreport", |
| ) |
| group.addoption( |
| "--doctest-glob", |
| action="append", |
| default=[], |
| metavar="pat", |
| help="Doctests file matching pattern, default: test*.txt", |
| dest="doctestglob", |
| ) |
| group.addoption( |
| "--doctest-ignore-import-errors", |
| action="store_true", |
| default=False, |
| help="Ignore doctest collection errors", |
| dest="doctest_ignore_import_errors", |
| ) |
| group.addoption( |
| "--doctest-continue-on-failure", |
| action="store_true", |
| default=False, |
| help="For a given doctest, continue to run after the first failure", |
| dest="doctest_continue_on_failure", |
| ) |
|
|
|
|
| def pytest_unconfigure() -> None: |
| global RUNNER_CLASS |
|
|
| RUNNER_CLASS = None |
|
|
|
|
| def pytest_collect_file( |
| file_path: Path, |
| parent: Collector, |
| ) -> DoctestModule | DoctestTextfile | None: |
| config = parent.config |
| if file_path.suffix == ".py": |
| if config.option.doctestmodules and not any( |
| (_is_setup_py(file_path), _is_main_py(file_path)) |
| ): |
| return DoctestModule.from_parent(parent, path=file_path) |
| elif _is_doctest(config, file_path, parent): |
| return DoctestTextfile.from_parent(parent, path=file_path) |
| return None |
|
|
|
|
| def _is_setup_py(path: Path) -> bool: |
| if path.name != "setup.py": |
| return False |
| contents = path.read_bytes() |
| return b"setuptools" in contents or b"distutils" in contents |
|
|
|
|
| def _is_doctest(config: Config, path: Path, parent: Collector) -> bool: |
| if path.suffix in (".txt", ".rst") and parent.session.isinitpath(path): |
| return True |
| globs = config.getoption("doctestglob") or ["test*.txt"] |
| return any(fnmatch_ex(glob, path) for glob in globs) |
|
|
|
|
| def _is_main_py(path: Path) -> bool: |
| return path.name == "__main__.py" |
|
|
|
|
| class ReprFailDoctest(TerminalRepr): |
| def __init__( |
| self, reprlocation_lines: Sequence[tuple[ReprFileLocation, Sequence[str]]] |
| ) -> None: |
| self.reprlocation_lines = reprlocation_lines |
|
|
| def toterminal(self, tw: TerminalWriter) -> None: |
| for reprlocation, lines in self.reprlocation_lines: |
| for line in lines: |
| tw.line(line) |
| reprlocation.toterminal(tw) |
|
|
|
|
| class MultipleDoctestFailures(Exception): |
| def __init__(self, failures: Sequence[doctest.DocTestFailure]) -> None: |
| super().__init__() |
| self.failures = failures |
|
|
|
|
| def _init_runner_class() -> type[doctest.DocTestRunner]: |
| import doctest |
|
|
| class PytestDoctestRunner(doctest.DebugRunner): |
| """Runner to collect failures. |
| |
| Note that the out variable in this case is a list instead of a |
| stdout-like object. |
| """ |
|
|
| def __init__( |
| self, |
| checker: doctest.OutputChecker | None = None, |
| verbose: bool | None = None, |
| optionflags: int = 0, |
| continue_on_failure: bool = True, |
| ) -> None: |
| super().__init__(checker=checker, verbose=verbose, optionflags=optionflags) |
| self.continue_on_failure = continue_on_failure |
|
|
| def report_failure( |
| self, |
| out, |
| test: doctest.DocTest, |
| example: doctest.Example, |
| got: str, |
| ) -> None: |
| failure = doctest.DocTestFailure(test, example, got) |
| if self.continue_on_failure: |
| out.append(failure) |
| else: |
| raise failure |
|
|
| def report_unexpected_exception( |
| self, |
| out, |
| test: doctest.DocTest, |
| example: doctest.Example, |
| exc_info: tuple[type[BaseException], BaseException, types.TracebackType], |
| ) -> None: |
| if isinstance(exc_info[1], OutcomeException): |
| raise exc_info[1] |
| if isinstance(exc_info[1], bdb.BdbQuit): |
| outcomes.exit("Quitting debugger") |
| failure = doctest.UnexpectedException(test, example, exc_info) |
| if self.continue_on_failure: |
| out.append(failure) |
| else: |
| raise failure |
|
|
| return PytestDoctestRunner |
|
|
|
|
| def _get_runner( |
| checker: doctest.OutputChecker | None = None, |
| verbose: bool | None = None, |
| optionflags: int = 0, |
| continue_on_failure: bool = True, |
| ) -> doctest.DocTestRunner: |
| |
| global RUNNER_CLASS |
| if RUNNER_CLASS is None: |
| RUNNER_CLASS = _init_runner_class() |
| |
| |
| return RUNNER_CLASS( |
| checker=checker, |
| verbose=verbose, |
| optionflags=optionflags, |
| continue_on_failure=continue_on_failure, |
| ) |
|
|
|
|
| class DoctestItem(Item): |
| def __init__( |
| self, |
| name: str, |
| parent: DoctestTextfile | DoctestModule, |
| runner: doctest.DocTestRunner, |
| dtest: doctest.DocTest, |
| ) -> None: |
| super().__init__(name, parent) |
| self.runner = runner |
| self.dtest = dtest |
|
|
| |
| self.obj = None |
| fm = self.session._fixturemanager |
| fixtureinfo = fm.getfixtureinfo(node=self, func=None, cls=None) |
| self._fixtureinfo = fixtureinfo |
| self.fixturenames = fixtureinfo.names_closure |
| self._initrequest() |
|
|
| @classmethod |
| def from_parent( |
| cls, |
| parent: DoctestTextfile | DoctestModule, |
| *, |
| name: str, |
| runner: doctest.DocTestRunner, |
| dtest: doctest.DocTest, |
| ) -> Self: |
| |
| """The public named constructor.""" |
| return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest) |
|
|
| def _initrequest(self) -> None: |
| self.funcargs: dict[str, object] = {} |
| self._request = TopRequest(self, _ispytest=True) |
|
|
| def setup(self) -> None: |
| self._request._fillfixtures() |
| globs = dict(getfixture=self._request.getfixturevalue) |
| for name, value in self._request.getfixturevalue("doctest_namespace").items(): |
| globs[name] = value |
| self.dtest.globs.update(globs) |
|
|
| def runtest(self) -> None: |
| _check_all_skipped(self.dtest) |
| self._disable_output_capturing_for_darwin() |
| failures: list[doctest.DocTestFailure] = [] |
| |
| |
| self.runner.run(self.dtest, out=failures) |
| if failures: |
| raise MultipleDoctestFailures(failures) |
|
|
| def _disable_output_capturing_for_darwin(self) -> None: |
| """Disable output capturing. Otherwise, stdout is lost to doctest (#985).""" |
| if platform.system() != "Darwin": |
| return |
| capman = self.config.pluginmanager.getplugin("capturemanager") |
| if capman: |
| capman.suspend_global_capture(in_=True) |
| out, err = capman.read_global_capture() |
| sys.stdout.write(out) |
| sys.stderr.write(err) |
|
|
| |
| def repr_failure( |
| self, |
| excinfo: ExceptionInfo[BaseException], |
| ) -> str | TerminalRepr: |
| import doctest |
|
|
| failures: ( |
| Sequence[doctest.DocTestFailure | doctest.UnexpectedException] | None |
| ) = None |
| if isinstance( |
| excinfo.value, (doctest.DocTestFailure, doctest.UnexpectedException) |
| ): |
| failures = [excinfo.value] |
| elif isinstance(excinfo.value, MultipleDoctestFailures): |
| failures = excinfo.value.failures |
|
|
| if failures is None: |
| return super().repr_failure(excinfo) |
|
|
| reprlocation_lines = [] |
| for failure in failures: |
| example = failure.example |
| test = failure.test |
| filename = test.filename |
| if test.lineno is None: |
| lineno = None |
| else: |
| lineno = test.lineno + example.lineno + 1 |
| message = type(failure).__name__ |
| |
| reprlocation = ReprFileLocation(filename, lineno, message) |
| checker = _get_checker() |
| report_choice = _get_report_choice(self.config.getoption("doctestreport")) |
| if lineno is not None: |
| assert failure.test.docstring is not None |
| lines = failure.test.docstring.splitlines(False) |
| |
| assert test.lineno is not None |
| lines = [ |
| f"{i + test.lineno + 1:03d} {x}" for (i, x) in enumerate(lines) |
| ] |
| |
| lines = lines[max(example.lineno - 9, 0) : example.lineno + 1] |
| else: |
| lines = [ |
| "EXAMPLE LOCATION UNKNOWN, not showing all tests of that example" |
| ] |
| indent = ">>>" |
| for line in example.source.splitlines(): |
| lines.append(f"??? {indent} {line}") |
| indent = "..." |
| if isinstance(failure, doctest.DocTestFailure): |
| lines += checker.output_difference( |
| example, failure.got, report_choice |
| ).split("\n") |
| else: |
| inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info) |
| lines += [f"UNEXPECTED EXCEPTION: {inner_excinfo.value!r}"] |
| lines += [ |
| x.strip("\n") for x in traceback.format_exception(*failure.exc_info) |
| ] |
| reprlocation_lines.append((reprlocation, lines)) |
| return ReprFailDoctest(reprlocation_lines) |
|
|
| def reportinfo(self) -> tuple[os.PathLike[str] | str, int | None, str]: |
| return self.path, self.dtest.lineno, f"[doctest] {self.name}" |
|
|
|
|
| def _get_flag_lookup() -> dict[str, int]: |
| import doctest |
|
|
| return dict( |
| DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1, |
| DONT_ACCEPT_BLANKLINE=doctest.DONT_ACCEPT_BLANKLINE, |
| NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE, |
| ELLIPSIS=doctest.ELLIPSIS, |
| IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL, |
| COMPARISON_FLAGS=doctest.COMPARISON_FLAGS, |
| ALLOW_UNICODE=_get_allow_unicode_flag(), |
| ALLOW_BYTES=_get_allow_bytes_flag(), |
| NUMBER=_get_number_flag(), |
| ) |
|
|
|
|
| def get_optionflags(config: Config) -> int: |
| optionflags_str = config.getini("doctest_optionflags") |
| flag_lookup_table = _get_flag_lookup() |
| flag_acc = 0 |
| for flag in optionflags_str: |
| flag_acc |= flag_lookup_table[flag] |
| return flag_acc |
|
|
|
|
| def _get_continue_on_failure(config: Config) -> bool: |
| continue_on_failure: bool = config.getvalue("doctest_continue_on_failure") |
| if continue_on_failure: |
| |
| |
| if config.getvalue("usepdb"): |
| continue_on_failure = False |
| return continue_on_failure |
|
|
|
|
| class DoctestTextfile(Module): |
| obj = None |
|
|
| def collect(self) -> Iterable[DoctestItem]: |
| import doctest |
|
|
| |
| |
| encoding = self.config.getini("doctest_encoding") |
| text = self.path.read_text(encoding) |
| filename = str(self.path) |
| name = self.path.name |
| globs = {"__name__": "__main__"} |
|
|
| optionflags = get_optionflags(self.config) |
|
|
| runner = _get_runner( |
| verbose=False, |
| optionflags=optionflags, |
| checker=_get_checker(), |
| continue_on_failure=_get_continue_on_failure(self.config), |
| ) |
|
|
| parser = doctest.DocTestParser() |
| test = parser.get_doctest(text, globs, name, filename, 0) |
| if test.examples: |
| yield DoctestItem.from_parent( |
| self, name=test.name, runner=runner, dtest=test |
| ) |
|
|
|
|
| def _check_all_skipped(test: doctest.DocTest) -> None: |
| """Raise pytest.skip() if all examples in the given DocTest have the SKIP |
| option set.""" |
| import doctest |
|
|
| all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples) |
| if all_skipped: |
| skip("all tests skipped by +SKIP option") |
|
|
|
|
| def _is_mocked(obj: object) -> bool: |
| """Return if an object is possibly a mock object by checking the |
| existence of a highly improbable attribute.""" |
| return ( |
| safe_getattr(obj, "pytest_mock_example_attribute_that_shouldnt_exist", None) |
| is not None |
| ) |
|
|
|
|
| @contextmanager |
| def _patch_unwrap_mock_aware() -> Generator[None]: |
| """Context manager which replaces ``inspect.unwrap`` with a version |
| that's aware of mock objects and doesn't recurse into them.""" |
| real_unwrap = inspect.unwrap |
|
|
| def _mock_aware_unwrap( |
| func: Callable[..., Any], *, stop: Callable[[Any], Any] | None = None |
| ) -> Any: |
| try: |
| if stop is None or stop is _is_mocked: |
| return real_unwrap(func, stop=_is_mocked) |
| _stop = stop |
| return real_unwrap(func, stop=lambda obj: _is_mocked(obj) or _stop(func)) |
| except Exception as e: |
| warnings.warn( |
| f"Got {e!r} when unwrapping {func!r}. This is usually caused " |
| "by a violation of Python's object protocol; see e.g. " |
| "https://github.com/pytest-dev/pytest/issues/5080", |
| PytestWarning, |
| ) |
| raise |
|
|
| inspect.unwrap = _mock_aware_unwrap |
| try: |
| yield |
| finally: |
| inspect.unwrap = real_unwrap |
|
|
|
|
| class DoctestModule(Module): |
| def collect(self) -> Iterable[DoctestItem]: |
| import doctest |
|
|
| class MockAwareDocTestFinder(doctest.DocTestFinder): |
| py_ver_info_minor = sys.version_info[:2] |
| is_find_lineno_broken = ( |
| py_ver_info_minor < (3, 11) |
| or (py_ver_info_minor == (3, 11) and sys.version_info.micro < 9) |
| or (py_ver_info_minor == (3, 12) and sys.version_info.micro < 3) |
| ) |
| if is_find_lineno_broken: |
|
|
| def _find_lineno(self, obj, source_lines): |
| """On older Pythons, doctest code does not take into account |
| `@property`. https://github.com/python/cpython/issues/61648 |
| |
| Moreover, wrapped Doctests need to be unwrapped so the correct |
| line number is returned. #8796 |
| """ |
| if isinstance(obj, property): |
| obj = getattr(obj, "fget", obj) |
|
|
| if hasattr(obj, "__wrapped__"): |
| |
| obj = inspect.unwrap(obj) |
|
|
| |
| return super()._find_lineno( |
| obj, |
| source_lines, |
| ) |
|
|
| if sys.version_info < (3, 10): |
|
|
| def _find( |
| self, tests, obj, name, module, source_lines, globs, seen |
| ) -> None: |
| """Override _find to work around issue in stdlib. |
| |
| https://github.com/pytest-dev/pytest/issues/3456 |
| https://github.com/python/cpython/issues/69718 |
| """ |
| if _is_mocked(obj): |
| return |
| with _patch_unwrap_mock_aware(): |
| |
| super()._find( |
| tests, obj, name, module, source_lines, globs, seen |
| ) |
|
|
| if sys.version_info < (3, 13): |
|
|
| def _from_module(self, module, object): |
| """`cached_property` objects are never considered a part |
| of the 'current module'. As such they are skipped by doctest. |
| Here we override `_from_module` to check the underlying |
| function instead. https://github.com/python/cpython/issues/107995 |
| """ |
| if isinstance(object, functools.cached_property): |
| object = object.func |
|
|
| |
| return super()._from_module(module, object) |
|
|
| try: |
| module = self.obj |
| except Collector.CollectError: |
| if self.config.getvalue("doctest_ignore_import_errors"): |
| skip(f"unable to import module {self.path!r}") |
| else: |
| raise |
|
|
| |
| |
| self.session._fixturemanager.parsefactories(self) |
|
|
| |
| finder = MockAwareDocTestFinder() |
| optionflags = get_optionflags(self.config) |
| runner = _get_runner( |
| verbose=False, |
| optionflags=optionflags, |
| checker=_get_checker(), |
| continue_on_failure=_get_continue_on_failure(self.config), |
| ) |
|
|
| for test in finder.find(module, module.__name__): |
| if test.examples: |
| yield DoctestItem.from_parent( |
| self, name=test.name, runner=runner, dtest=test |
| ) |
|
|
|
|
| def _init_checker_class() -> type[doctest.OutputChecker]: |
| import doctest |
|
|
| class LiteralsOutputChecker(doctest.OutputChecker): |
| |
| |
| |
|
|
| _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE) |
| _bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE) |
| _number_re = re.compile( |
| r""" |
| (?P<number> |
| (?P<mantissa> |
| (?P<integer1> [+-]?\d*)\.(?P<fraction>\d+) |
| | |
| (?P<integer2> [+-]?\d+)\. |
| ) |
| (?: |
| [Ee] |
| (?P<exponent1> [+-]?\d+) |
| )? |
| | |
| (?P<integer3> [+-]?\d+) |
| (?: |
| [Ee] |
| (?P<exponent2> [+-]?\d+) |
| ) |
| ) |
| """, |
| re.VERBOSE, |
| ) |
|
|
| def check_output(self, want: str, got: str, optionflags: int) -> bool: |
| if super().check_output(want, got, optionflags): |
| return True |
|
|
| allow_unicode = optionflags & _get_allow_unicode_flag() |
| allow_bytes = optionflags & _get_allow_bytes_flag() |
| allow_number = optionflags & _get_number_flag() |
|
|
| if not allow_unicode and not allow_bytes and not allow_number: |
| return False |
|
|
| def remove_prefixes(regex: re.Pattern[str], txt: str) -> str: |
| return re.sub(regex, r"\1\2", txt) |
|
|
| if allow_unicode: |
| want = remove_prefixes(self._unicode_literal_re, want) |
| got = remove_prefixes(self._unicode_literal_re, got) |
|
|
| if allow_bytes: |
| want = remove_prefixes(self._bytes_literal_re, want) |
| got = remove_prefixes(self._bytes_literal_re, got) |
|
|
| if allow_number: |
| got = self._remove_unwanted_precision(want, got) |
|
|
| return super().check_output(want, got, optionflags) |
|
|
| def _remove_unwanted_precision(self, want: str, got: str) -> str: |
| wants = list(self._number_re.finditer(want)) |
| gots = list(self._number_re.finditer(got)) |
| if len(wants) != len(gots): |
| return got |
| offset = 0 |
| for w, g in zip(wants, gots): |
| fraction: str | None = w.group("fraction") |
| exponent: str | None = w.group("exponent1") |
| if exponent is None: |
| exponent = w.group("exponent2") |
| precision = 0 if fraction is None else len(fraction) |
| if exponent is not None: |
| precision -= int(exponent) |
| if float(w.group()) == approx(float(g.group()), abs=10**-precision): |
| |
| |
| |
| got = ( |
| got[: g.start() + offset] + w.group() + got[g.end() + offset :] |
| ) |
| offset += w.end() - w.start() - (g.end() - g.start()) |
| return got |
|
|
| return LiteralsOutputChecker |
|
|
|
|
| def _get_checker() -> doctest.OutputChecker: |
| """Return a doctest.OutputChecker subclass that supports some |
| additional options: |
| |
| * ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b'' |
| prefixes (respectively) in string literals. Useful when the same |
| doctest should run in Python 2 and Python 3. |
| |
| * NUMBER to ignore floating-point differences smaller than the |
| precision of the literal number in the doctest. |
| |
| An inner class is used to avoid importing "doctest" at the module |
| level. |
| """ |
| global CHECKER_CLASS |
| if CHECKER_CLASS is None: |
| CHECKER_CLASS = _init_checker_class() |
| return CHECKER_CLASS() |
|
|
|
|
| def _get_allow_unicode_flag() -> int: |
| """Register and return the ALLOW_UNICODE flag.""" |
| import doctest |
|
|
| return doctest.register_optionflag("ALLOW_UNICODE") |
|
|
|
|
| def _get_allow_bytes_flag() -> int: |
| """Register and return the ALLOW_BYTES flag.""" |
| import doctest |
|
|
| return doctest.register_optionflag("ALLOW_BYTES") |
|
|
|
|
| def _get_number_flag() -> int: |
| """Register and return the NUMBER flag.""" |
| import doctest |
|
|
| return doctest.register_optionflag("NUMBER") |
|
|
|
|
| def _get_report_choice(key: str) -> int: |
| """Return the actual `doctest` module flag value. |
| |
| We want to do it as late as possible to avoid importing `doctest` and all |
| its dependencies when parsing options, as it adds overhead and breaks tests. |
| """ |
| import doctest |
|
|
| return { |
| DOCTEST_REPORT_CHOICE_UDIFF: doctest.REPORT_UDIFF, |
| DOCTEST_REPORT_CHOICE_CDIFF: doctest.REPORT_CDIFF, |
| DOCTEST_REPORT_CHOICE_NDIFF: doctest.REPORT_NDIFF, |
| DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE: doctest.REPORT_ONLY_FIRST_FAILURE, |
| DOCTEST_REPORT_CHOICE_NONE: 0, |
| }[key] |
|
|
|
|
| @fixture(scope="session") |
| def doctest_namespace() -> dict[str, Any]: |
| """Fixture that returns a :py:class:`dict` that will be injected into the |
| namespace of doctests. |
| |
| Usually this fixture is used in conjunction with another ``autouse`` fixture: |
| |
| .. code-block:: python |
| |
| @pytest.fixture(autouse=True) |
| def add_np(doctest_namespace): |
| doctest_namespace["np"] = numpy |
| |
| For more details: :ref:`doctest_namespace`. |
| """ |
| return dict() |
|
|