| | |
| | |
| | |
| | |
| | |
| | """Discover and run ipdoctests in modules and test files.""" |
| | import bdb |
| | import builtins |
| | import inspect |
| | import os |
| | import platform |
| | import sys |
| | import traceback |
| | import types |
| | import warnings |
| | from contextlib import contextmanager |
| | from pathlib import Path |
| | from typing import ( |
| | TYPE_CHECKING, |
| | Any, |
| | Callable, |
| | Dict, |
| | Generator, |
| | Iterable, |
| | List, |
| | Optional, |
| | Pattern, |
| | Sequence, |
| | Tuple, |
| | Type, |
| | Union, |
| | ) |
| |
|
| | import pytest |
| | from _pytest import outcomes |
| | from _pytest._code.code import ExceptionInfo, ReprFileLocation, 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 FixtureRequest |
| | from _pytest.nodes import Collector |
| | from _pytest.outcomes import OutcomeException |
| | from _pytest.pathlib import fnmatch_ex, import_path |
| | from _pytest.python_api import approx |
| | from _pytest.warning_types import PytestWarning |
| |
|
| | if TYPE_CHECKING: |
| | import doctest |
| |
|
| | from .ipdoctest import IPDoctestOutputChecker |
| |
|
| | 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: Optional[Type["IPDoctestOutputChecker"]] = None |
| |
|
| |
|
| | def pytest_addoption(parser: Parser) -> None: |
| | parser.addini( |
| | "ipdoctest_optionflags", |
| | "option flags for ipdoctests", |
| | type="args", |
| | default=["ELLIPSIS"], |
| | ) |
| | parser.addini( |
| | "ipdoctest_encoding", "encoding used for ipdoctest files", default="utf-8" |
| | ) |
| | group = parser.getgroup("collect") |
| | group.addoption( |
| | "--ipdoctest-modules", |
| | action="store_true", |
| | default=False, |
| | help="run ipdoctests in all .py modules", |
| | dest="ipdoctestmodules", |
| | ) |
| | group.addoption( |
| | "--ipdoctest-report", |
| | type=str.lower, |
| | default="udiff", |
| | help="choose another output format for diffs on ipdoctest failure", |
| | choices=DOCTEST_REPORT_CHOICES, |
| | dest="ipdoctestreport", |
| | ) |
| | group.addoption( |
| | "--ipdoctest-glob", |
| | action="append", |
| | default=[], |
| | metavar="pat", |
| | help="ipdoctests file matching pattern, default: test*.txt", |
| | dest="ipdoctestglob", |
| | ) |
| | group.addoption( |
| | "--ipdoctest-ignore-import-errors", |
| | action="store_true", |
| | default=False, |
| | help="ignore ipdoctest ImportErrors", |
| | dest="ipdoctest_ignore_import_errors", |
| | ) |
| | group.addoption( |
| | "--ipdoctest-continue-on-failure", |
| | action="store_true", |
| | default=False, |
| | help="for a given ipdoctest, continue to run after the first failure", |
| | dest="ipdoctest_continue_on_failure", |
| | ) |
| |
|
| |
|
| | def pytest_unconfigure() -> None: |
| | global RUNNER_CLASS |
| |
|
| | RUNNER_CLASS = None |
| |
|
| |
|
| | def pytest_collect_file( |
| | file_path: Path, |
| | parent: Collector, |
| | ) -> Optional[Union["IPDoctestModule", "IPDoctestTextfile"]]: |
| | config = parent.config |
| | if file_path.suffix == ".py": |
| | if config.option.ipdoctestmodules and not any( |
| | (_is_setup_py(file_path), _is_main_py(file_path)) |
| | ): |
| | mod: IPDoctestModule = IPDoctestModule.from_parent(parent, path=file_path) |
| | return mod |
| | elif _is_ipdoctest(config, file_path, parent): |
| | txt: IPDoctestTextfile = IPDoctestTextfile.from_parent(parent, path=file_path) |
| | return txt |
| | return None |
| |
|
| |
|
| | if int(pytest.__version__.split(".")[0]) < 7: |
| | _collect_file = pytest_collect_file |
| |
|
| | def pytest_collect_file( |
| | path, |
| | parent: Collector, |
| | ) -> Optional[Union["IPDoctestModule", "IPDoctestTextfile"]]: |
| | return _collect_file(Path(path), parent) |
| |
|
| | _import_path = import_path |
| |
|
| | def import_path(path, root): |
| | import py.path |
| |
|
| | return _import_path(py.path.local(path)) |
| |
|
| |
|
| | 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_ipdoctest(config: Config, path: Path, parent: Collector) -> bool: |
| | if path.suffix in (".txt", ".rst") and parent.session.isinitpath(path): |
| | return True |
| | globs = config.getoption("ipdoctestglob") 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["IPDocTestRunner"]: |
| | import doctest |
| | from .ipdoctest import IPDocTestRunner |
| |
|
| | class PytestDoctestRunner(IPDocTestRunner): |
| | """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: Optional["IPDoctestOutputChecker"] = None, |
| | verbose: Optional[bool] = 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: Optional["IPDoctestOutputChecker"] = None, |
| | verbose: Optional[bool] = None, |
| | optionflags: int = 0, |
| | continue_on_failure: bool = True, |
| | ) -> "IPDocTestRunner": |
| | |
| | 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 IPDoctestItem(pytest.Item): |
| | _user_ns_orig: Dict[str, Any] |
| |
|
| | def __init__( |
| | self, |
| | name: str, |
| | parent: "Union[IPDoctestTextfile, IPDoctestModule]", |
| | runner: Optional["IPDocTestRunner"] = None, |
| | dtest: Optional["doctest.DocTest"] = None, |
| | ) -> None: |
| | super().__init__(name, parent) |
| | self.runner = runner |
| | self.dtest = dtest |
| | self.obj = None |
| | self.fixture_request: Optional[FixtureRequest] = None |
| | self._user_ns_orig = {} |
| |
|
| | @classmethod |
| | def from_parent( |
| | cls, |
| | parent: "Union[IPDoctestTextfile, IPDoctestModule]", |
| | *, |
| | name: str, |
| | runner: "IPDocTestRunner", |
| | dtest: "doctest.DocTest", |
| | ): |
| | |
| | """The public named constructor.""" |
| | return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest) |
| |
|
| | def setup(self) -> None: |
| | if self.dtest is not None: |
| | self.fixture_request = _setup_fixtures(self) |
| | globs = dict(getfixture=self.fixture_request.getfixturevalue) |
| | for name, value in self.fixture_request.getfixturevalue( |
| | "ipdoctest_namespace" |
| | ).items(): |
| | globs[name] = value |
| | self.dtest.globs.update(globs) |
| |
|
| | from .ipdoctest import IPExample |
| |
|
| | if isinstance(self.dtest.examples[0], IPExample): |
| | |
| | |
| | |
| | self._user_ns_orig = {} |
| | self._user_ns_orig.update(_ip.user_ns) |
| | _ip.user_ns.update(self.dtest.globs) |
| | |
| | |
| | _ip.user_ns.pop("_", None) |
| | _ip.user_ns["__builtins__"] = builtins |
| | self.dtest.globs = _ip.user_ns |
| |
|
| | def teardown(self) -> None: |
| | from .ipdoctest import IPExample |
| |
|
| | |
| | if isinstance(self.dtest.examples[0], IPExample): |
| | self.dtest.globs = {} |
| | _ip.user_ns.clear() |
| | _ip.user_ns.update(self._user_ns_orig) |
| | del self._user_ns_orig |
| |
|
| | self.dtest.globs.clear() |
| |
|
| | def runtest(self) -> None: |
| | assert self.dtest is not None |
| | assert self.runner is not None |
| | _check_all_skipped(self.dtest) |
| | self._disable_output_capturing_for_darwin() |
| | failures: List["doctest.DocTestFailure"] = [] |
| |
|
| | |
| | had_underscore_value = hasattr(builtins, "_") |
| | underscore_original_value = getattr(builtins, "_", None) |
| |
|
| | |
| | |
| | |
| | curdir = os.getcwd() |
| | os.chdir(self.fspath.dirname) |
| | try: |
| | |
| | |
| | self.runner.run(self.dtest, out=failures, clear_globs=False) |
| | finally: |
| | os.chdir(curdir) |
| | if had_underscore_value: |
| | setattr(builtins, "_", underscore_original_value) |
| | elif hasattr(builtins, "_"): |
| | delattr(builtins, "_") |
| |
|
| | if failures: |
| | raise MultipleDoctestFailures(failures) |
| |
|
| | def _disable_output_capturing_for_darwin(self) -> None: |
| | """Disable output capturing. Otherwise, stdout is lost to ipdoctest (pytest#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], |
| | ) -> Union[str, TerminalRepr]: |
| | import doctest |
| |
|
| | failures: Optional[ |
| | Sequence[Union[doctest.DocTestFailure, doctest.UnexpectedException]] |
| | ] = 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("ipdoctestreport")) |
| | 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 += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)] |
| | 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[Union["os.PathLike[str]", str], Optional[int], str]: |
| | assert self.dtest is not None |
| | return self.path, self.dtest.lineno, "[ipdoctest] %s" % self.name |
| |
|
| | if int(pytest.__version__.split(".")[0]) < 7: |
| |
|
| | @property |
| | def path(self) -> Path: |
| | return Path(self.fspath) |
| |
|
| |
|
| | 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(parent): |
| | optionflags_str = parent.config.getini("ipdoctest_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): |
| | continue_on_failure = config.getvalue("ipdoctest_continue_on_failure") |
| | if continue_on_failure: |
| | |
| | |
| | if config.getvalue("usepdb"): |
| | continue_on_failure = False |
| | return continue_on_failure |
| |
|
| |
|
| | class IPDoctestTextfile(pytest.Module): |
| | obj = None |
| |
|
| | def collect(self) -> Iterable[IPDoctestItem]: |
| | import doctest |
| | from .ipdoctest import IPDocTestParser |
| |
|
| | |
| | |
| | encoding = self.config.getini("ipdoctest_encoding") |
| | text = self.path.read_text(encoding) |
| | filename = str(self.path) |
| | name = self.path.name |
| | globs = {"__name__": "__main__"} |
| |
|
| | optionflags = get_optionflags(self) |
| |
|
| | runner = _get_runner( |
| | verbose=False, |
| | optionflags=optionflags, |
| | checker=_get_checker(), |
| | continue_on_failure=_get_continue_on_failure(self.config), |
| | ) |
| |
|
| | parser = IPDocTestParser() |
| | test = parser.get_doctest(text, globs, name, filename, 0) |
| | if test.examples: |
| | yield IPDoctestItem.from_parent( |
| | self, name=test.name, runner=runner, dtest=test |
| | ) |
| |
|
| | if int(pytest.__version__.split(".")[0]) < 7: |
| |
|
| | @property |
| | def path(self) -> Path: |
| | return Path(self.fspath) |
| |
|
| | @classmethod |
| | def from_parent( |
| | cls, |
| | parent, |
| | *, |
| | fspath=None, |
| | path: Optional[Path] = None, |
| | **kw, |
| | ): |
| | if path is not None: |
| | import py.path |
| |
|
| | fspath = py.path.local(path) |
| | return super().from_parent(parent=parent, fspath=fspath, **kw) |
| |
|
| |
|
| | 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: |
| | pytest.skip("all docstests 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, None, 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: Optional[Callable[[Any], Any]] = 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( |
| | "Got %r when unwrapping %r. This is usually caused " |
| | "by a violation of Python's object protocol; see e.g. " |
| | "https://github.com/pytest-dev/pytest/issues/5080" % (e, func), |
| | PytestWarning, |
| | ) |
| | raise |
| |
|
| | inspect.unwrap = _mock_aware_unwrap |
| | try: |
| | yield |
| | finally: |
| | inspect.unwrap = real_unwrap |
| |
|
| |
|
| | class IPDoctestModule(pytest.Module): |
| | def collect(self) -> Iterable[IPDoctestItem]: |
| | import doctest |
| | from .ipdoctest import DocTestFinder, IPDocTestParser |
| |
|
| | class MockAwareDocTestFinder(DocTestFinder): |
| | """A hackish ipdoctest finder that overrides stdlib internals to fix a stdlib bug. |
| | |
| | https://github.com/pytest-dev/pytest/issues/3456 |
| | https://bugs.python.org/issue25532 |
| | """ |
| |
|
| | def _find_lineno(self, obj, source_lines): |
| | """Doctest code does not take into account `@property`, this |
| | is a hackish way to fix it. https://bugs.python.org/issue17446 |
| | |
| | Wrapped Doctests will need to be unwrapped so the correct |
| | line number is returned. This will be reported upstream. #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, |
| | ) |
| |
|
| | def _find( |
| | self, tests, obj, name, module, source_lines, globs, seen |
| | ) -> None: |
| | if _is_mocked(obj): |
| | return |
| | with _patch_unwrap_mock_aware(): |
| | |
| | super()._find( |
| | tests, obj, name, module, source_lines, globs, seen |
| | ) |
| |
|
| | if self.path.name == "conftest.py": |
| | if int(pytest.__version__.split(".")[0]) < 7: |
| | module = self.config.pluginmanager._importconftest( |
| | self.path, |
| | self.config.getoption("importmode"), |
| | ) |
| | else: |
| | module = self.config.pluginmanager._importconftest( |
| | self.path, |
| | self.config.getoption("importmode"), |
| | rootpath=self.config.rootpath, |
| | ) |
| | else: |
| | try: |
| | module = import_path(self.path, root=self.config.rootpath) |
| | except ImportError: |
| | if self.config.getvalue("ipdoctest_ignore_import_errors"): |
| | pytest.skip("unable to import module %r" % self.path) |
| | else: |
| | raise |
| | |
| | finder = MockAwareDocTestFinder(parser=IPDocTestParser()) |
| | optionflags = get_optionflags(self) |
| | 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 IPDoctestItem.from_parent( |
| | self, name=test.name, runner=runner, dtest=test |
| | ) |
| |
|
| | if int(pytest.__version__.split(".")[0]) < 7: |
| |
|
| | @property |
| | def path(self) -> Path: |
| | return Path(self.fspath) |
| |
|
| | @classmethod |
| | def from_parent( |
| | cls, |
| | parent, |
| | *, |
| | fspath=None, |
| | path: Optional[Path] = None, |
| | **kw, |
| | ): |
| | if path is not None: |
| | import py.path |
| |
|
| | fspath = py.path.local(path) |
| | return super().from_parent(parent=parent, fspath=fspath, **kw) |
| |
|
| |
|
| | def _setup_fixtures(doctest_item: IPDoctestItem) -> FixtureRequest: |
| | """Used by IPDoctestTextfile and IPDoctestItem to setup fixture information.""" |
| |
|
| | def func() -> None: |
| | pass |
| |
|
| | doctest_item.funcargs = {} |
| | fm = doctest_item.session._fixturemanager |
| | doctest_item._fixtureinfo = fm.getfixtureinfo( |
| | node=doctest_item, func=func, cls=None, funcargs=False |
| | ) |
| | fixture_request = FixtureRequest(doctest_item, _ispytest=True) |
| | fixture_request._fillfixtures() |
| | return fixture_request |
| |
|
| |
|
| | def _init_checker_class() -> Type["IPDoctestOutputChecker"]: |
| | import doctest |
| | import re |
| | from .ipdoctest import IPDoctestOutputChecker |
| |
|
| | class LiteralsOutputChecker(IPDoctestOutputChecker): |
| | |
| | |
| | |
| |
|
| | _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: Optional[str] = w.group("fraction") |
| | exponent: Optional[str] = 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() -> "IPDoctestOutputChecker": |
| | """Return a IPDoctestOutputChecker 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 |
| | ipdoctest should run in Python 2 and Python 3. |
| | |
| | * NUMBER to ignore floating-point differences smaller than the |
| | precision of the literal number in the ipdoctest. |
| | |
| | An inner class is used to avoid importing "ipdoctest" 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 `ipdoctest` module flag value. |
| | |
| | We want to do it as late as possible to avoid importing `ipdoctest` 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] |
| |
|
| |
|
| | @pytest.fixture(scope="session") |
| | def ipdoctest_namespace() -> Dict[str, Any]: |
| | """Fixture that returns a :py:class:`dict` that will be injected into the |
| | namespace of ipdoctests.""" |
| | return dict() |
| |
|