| | |
| | """Discover and run doctests in modules and test files.""" |
| |
|
| | from __future__ import annotations |
| |
|
| | import bdb |
| | from contextlib import contextmanager |
| | import functools |
| | import inspect |
| | import os |
| | from pathlib import Path |
| | import platform |
| | import sys |
| | import traceback |
| | import types |
| | from typing import Any |
| | from typing import Callable |
| | from typing import Generator |
| | from typing import Iterable |
| | from typing import Pattern |
| | from typing import Sequence |
| | 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 = [ |
| | "%03d %s" % (i + test.lineno + 1, 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 |
| | import re |
| |
|
| | 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: 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() |
| |
|