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