1"""Discover and run doctests in modules and test files."""

2 import bdb

3 import inspect

4 import os

5 import platform

6 import sys

7 import traceback

8 import types

9 import warnings

10 from contextlib import contextmanager

11 from pathlib import Path

12 from typing import Any

13 from typing import Callable

14 from typing import Dict

15 from typing import Generator

16 from typing import Iterable

17 from typing import List

18 from typing import Optional

19 from typing import Pattern

20 from typing import Sequence

21 from typing import Tuple

22 from typing import Type

23 from typing import TYPE_CHECKING

24 from typing import Union

25

26 from _pytest import outcomes

27 from _pytest._code.code import ExceptionInfo

28 from _pytest._code.code import ReprFileLocation

29 from _pytest._code.code import TerminalRepr

30 from _pytest._io import TerminalWriter

31 from _pytest.compat import safe_getattr

32 from _pytest.config import Config

33 from _pytest.config.argparsing import Parser

34 from _pytest.fixtures import fixture

35 from _pytest.fixtures import FixtureRequest

36 from _pytest.nodes import Collector

37 from _pytest.nodes import Item

38 from _pytest.outcomes import OutcomeException

39 from _pytest.outcomes import skip

40 from _pytest.pathlib import fnmatch_ex

41 from _pytest.pathlib import import_path

42 from _pytest.python import Module

43 from _pytest.python_api import approx

44 from _pytest.warning_types import PytestWarning

45

46 if TYPE_CHECKING:

47 import doctest

48

49 DOCTEST_REPORT_CHOICE_NONE = "none"

50 DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"

51 DOCTEST_REPORT_CHOICE_NDIFF = "ndiff"

52 DOCTEST_REPORT_CHOICE_UDIFF = "udiff"

53 DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE = "only_first_failure"

54

55 DOCTEST_REPORT_CHOICES = (

56 DOCTEST_REPORT_CHOICE_NONE,

57 DOCTEST_REPORT_CHOICE_CDIFF,

58 DOCTEST_REPORT_CHOICE_NDIFF,

59 DOCTEST_REPORT_CHOICE_UDIFF,

60 DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE,

61)

62

63# Lazy definition of runner class

64 RUNNER_CLASS = None

65# Lazy definition of output checker class

66 CHECKER_CLASS: Optional[Type["doctest.OutputChecker"]] = None

67

68

69 def pytest_addoption(parser: Parser) -> None:

70 parser.addini(

71 "doctest_optionflags",

72 "Option flags for doctests",

73 type="args",

74 default=["ELLIPSIS"],

75 )

76 parser.addini(

77 "doctest_encoding", "Encoding used for doctest files", default="utf-8"

78 )

79 group = parser.getgroup("collect")

80 group.addoption(

81 "--doctest-modules",

82 action="store_true",

83 default=False,

84 help="Run doctests in all .py modules",

85 dest="doctestmodules",

86 )

87 group.addoption(

88 "--doctest-report",

89 type=str.lower,

90 default="udiff",

91 help="Choose another output format for diffs on doctest failure",

92 choices=DOCTEST_REPORT_CHOICES,

93 dest="doctestreport",

94 )

95 group.addoption(

96 "--doctest-glob",

97 action="append",

98 default=[],

99 metavar="pat",

100 help="Doctests file matching pattern, default: test*.txt",

101 dest="doctestglob",

102 )

103 group.addoption(

104 "--doctest-ignore-import-errors",

105 action="store_true",

106 default=False,

107 help="Ignore doctest ImportErrors",

108 dest="doctest_ignore_import_errors",

109 )

110 group.addoption(

111 "--doctest-continue-on-failure",

112 action="store_true",

113 default=False,

114 help="For a given doctest, continue to run after the first failure",

115 dest="doctest_continue_on_failure",

116 )

117

118

119 def pytest_unconfigure() -> None:

120 global RUNNER_CLASS

121

122 RUNNER_CLASS = None

123

124

125 def pytest_collect_file(

126 file_path: Path,

127 parent: Collector,

128) -> Optional[Union["DoctestModule", "DoctestTextfile"]]:

129 config = parent.config

130 if file_path.suffix == ".py":

131 if config.option.doctestmodules and not any(

132 (_is_setup_py(file_path), _is_main_py(file_path))

133 ):

134 mod: DoctestModule = DoctestModule.from_parent(parent, path=file_path)

135 return mod

136 elif _is_doctest(config, file_path, parent):

137 txt: DoctestTextfile = DoctestTextfile.from_parent(parent, path=file_path)

138 return txt

139 return None

140

141

142 def _is_setup_py(path: Path) -> bool:

143 if path.name != "setup.py":

144 return False

145 contents = path.read_bytes()

146 return b"setuptools" in contents or b"distutils" in contents

147

148

149 def _is_doctest(config: Config, path: Path, parent: Collector) -> bool:

150 if path.suffix in (".txt", ".rst") and parent.session.isinitpath(path):

151 return True

152 globs = config.getoption("doctestglob") or ["test*.txt"]

153 return any(fnmatch_ex(glob, path) for glob in globs)

154

155

156 def _is_main_py(path: Path) -> bool:

157 return path.name == "__main__.py"

158

159

160 class ReprFailDoctest(TerminalRepr):

161 def __init__(

162 self, reprlocation_lines: Sequence[Tuple[ReprFileLocation, Sequence[str]]]

163 ) -> None:

164 self.reprlocation_lines = reprlocation_lines

165

166 def toterminal(self, tw: TerminalWriter) -> None:

167 for reprlocation, lines in self.reprlocation_lines:

168 for line in lines:

169 tw.line(line)

170 reprlocation.toterminal(tw)

171

172

173 class MultipleDoctestFailures(Exception):

174 def __init__(self, failures: Sequence["doctest.DocTestFailure"]) -> None:

175 super().__init__()

176 self.failures = failures

177

178

179 def _init_runner_class() -> Type["doctest.DocTestRunner"]:

180 import doctest

181

182 class PytestDoctestRunner(doctest.DebugRunner):

183 """Runner to collect failures.

184

185 Note that the out variable in this case is a list instead of a

186 stdout-like object.

187 """

188

189 def __init__(

190 self,

191 checker: Optional["doctest.OutputChecker"] = None,

192 verbose: Optional[bool] = None,

193 optionflags: int = 0,

194 continue_on_failure: bool = True,

195 ) -> None:

196 super().__init__(checker=checker, verbose=verbose, optionflags=optionflags)

197 self.continue_on_failure = continue_on_failure

198

199 def report_failure(

200 self,

201 out,

202 test: "doctest.DocTest",

203 example: "doctest.Example",

204 got: str,

205 ) -> None:

206 failure = doctest.DocTestFailure(test, example, got)

207 if self.continue_on_failure:

208 out.append(failure)

209 else:

210 raise failure

211

212 def report_unexpected_exception(

213 self,

214 out,

215 test: "doctest.DocTest",

216 example: "doctest.Example",

217 exc_info: Tuple[Type[BaseException], BaseException, types.TracebackType],

218 ) -> None:

219 if isinstance(exc_info[1], OutcomeException):

220 raise exc_info[1]

221 if isinstance(exc_info[1], bdb.BdbQuit):

222 outcomes.exit("Quitting debugger")

223 failure = doctest.UnexpectedException(test, example, exc_info)

224 if self.continue_on_failure:

225 out.append(failure)

226 else:

227 raise failure

228

229 return PytestDoctestRunner

230

231

232 def _get_runner(

233 checker: Optional["doctest.OutputChecker"] = None,

234 verbose: Optional[bool] = None,

235 optionflags: int = 0,

236 continue_on_failure: bool = True,

237) -> "doctest.DocTestRunner":

238 # We need this in order to do a lazy import on doctest

239 global RUNNER_CLASS

240 if RUNNER_CLASS is None:

241 RUNNER_CLASS = _init_runner_class()

242 # Type ignored because the continue_on_failure argument is only defined on

243 # PytestDoctestRunner, which is lazily defined so can't be used as a type.

244 return RUNNER_CLASS( # type: ignore

245 checker=checker,

246 verbose=verbose,

247 optionflags=optionflags,

248 continue_on_failure=continue_on_failure,

249 )

250

251

252 class DoctestItem(Item):

253 def __init__(

254 self,

255 name: str,

256 parent: "Union[DoctestTextfile, DoctestModule]",

257 runner: Optional["doctest.DocTestRunner"] = None,

258 dtest: Optional["doctest.DocTest"] = None,

259 ) -> None:

260 super().__init__(name, parent)

261 self.runner = runner

262 self.dtest = dtest

263 self.obj = None

264 self.fixture_request: Optional[FixtureRequest] = None

265

266 @classmethod

267 def from_parent( # type: ignore

268 cls,

269 parent: "Union[DoctestTextfile, DoctestModule]",

270 *,

271 name: str,

272 runner: "doctest.DocTestRunner",

273 dtest: "doctest.DocTest",

274 ):

275 # incompatible signature due to imposed limits on subclass

276 """The public named constructor."""

277 return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)

278

279 def setup(self) -> None:

280 if self.dtest is not None:

281 self.fixture_request = _setup_fixtures(self)

282 globs = dict(getfixture=self.fixture_request.getfixturevalue)

283 for name, value in self.fixture_request.getfixturevalue(

284 "doctest_namespace"

285 ).items():

286 globs[name] = value

287 self.dtest.globs.update(globs)

288

289 def runtest(self) -> None:

290 assert self.dtest is not None

291 assert self.runner is not None

292 _check_all_skipped(self.dtest)

293 self._disable_output_capturing_for_darwin()

294 failures: List["doctest.DocTestFailure"] = []

295 # Type ignored because we change the type of `out` from what

296 # doctest expects.

297 self.runner.run(self.dtest, out=failures) # type: ignore[arg-type]

298 if failures:

299 raise MultipleDoctestFailures(failures)

300

301 def _disable_output_capturing_for_darwin(self) -> None:

302 """Disable output capturing. Otherwise, stdout is lost to doctest (#985)."""

303 if platform.system() != "Darwin":

304 return

305 capman = self.config.pluginmanager.getplugin("capturemanager")

306 if capman:

307 capman.suspend_global_capture(in_=True)

308 out, err = capman.read_global_capture()

309 sys.stdout.write(out)

310 sys.stderr.write(err)

311

312 # TODO: Type ignored -- breaks Liskov Substitution.

313 def repr_failure( # type: ignore[override]

314 self,

315 excinfo: ExceptionInfo[BaseException],

316 ) -> Union[str, TerminalRepr]:

317 import doctest

318

319 failures: Optional[

320 Sequence[Union[doctest.DocTestFailure, doctest.UnexpectedException]]

321 ] = None

322 if isinstance(

323 excinfo.value, (doctest.DocTestFailure, doctest.UnexpectedException)

324 ):

325 failures = [excinfo.value]

326 elif isinstance(excinfo.value, MultipleDoctestFailures):

327 failures = excinfo.value.failures

328

329 if failures is None:

330 return super().repr_failure(excinfo)

331

332 reprlocation_lines = []

333 for failure in failures:

334 example = failure.example

335 test = failure.test

336 filename = test.filename

337 if test.lineno is None:

338 lineno = None

339 else:

340 lineno = test.lineno + example.lineno + 1

341 message = type(failure).__name__

342 # TODO: ReprFileLocation doesn't expect a None lineno.

343 reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type]

344 checker = _get_checker()

345 report_choice = _get_report_choice(self.config.getoption("doctestreport"))

346 if lineno is not None:

347 assert failure.test.docstring is not None

348 lines = failure.test.docstring.splitlines(False)

349 # add line numbers to the left of the error message

350 assert test.lineno is not None

351 lines = [

352 "%03d %s" % (i + test.lineno + 1, x) for (i, x) in enumerate(lines)

353 ]

354 # trim docstring error lines to 10

355 lines = lines[max(example.lineno - 9, 0) : example.lineno + 1]

356 else:

357 lines = [

358 "EXAMPLE LOCATION UNKNOWN, not showing all tests of that example"

359 ]

360 indent = ">>>"

361 for line in example.source.splitlines():

362 lines.append(f"??? {indent} {line}")

363 indent = "..."

364 if isinstance(failure, doctest.DocTestFailure):

365 lines += checker.output_difference(

366 example, failure.got, report_choice

367 ).split("\n")

368 else:

369 inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info)

370 lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)]

371 lines += [

372 x.strip("\n") for x in traceback.format_exception(*failure.exc_info)

373 ]

374 reprlocation_lines.append((reprlocation, lines))

375 return ReprFailDoctest(reprlocation_lines)

376

377 def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:

378 assert self.dtest is not None

379 return self.path, self.dtest.lineno, "[doctest] %s" % self.name

380

381

382 def _get_flag_lookup() -> Dict[str, int]:

383 import doctest

384

385 return dict(

386 DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1,

387 DONT_ACCEPT_BLANKLINE=doctest.DONT_ACCEPT_BLANKLINE,

388 NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE,

389 ELLIPSIS=doctest.ELLIPSIS,

390 IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL,

391 COMPARISON_FLAGS=doctest.COMPARISON_FLAGS,

392 ALLOW_UNICODE=_get_allow_unicode_flag(),

393 ALLOW_BYTES=_get_allow_bytes_flag(),

394 NUMBER=_get_number_flag(),

395 )

396

397

398 def get_optionflags(parent):

399 optionflags_str = parent.config.getini("doctest_optionflags")

400 flag_lookup_table = _get_flag_lookup()

401 flag_acc = 0

402 for flag in optionflags_str:

403 flag_acc |= flag_lookup_table[flag]

404 return flag_acc

405

406

407 def _get_continue_on_failure(config):

408 continue_on_failure = config.getvalue("doctest_continue_on_failure")

409 if continue_on_failure:

410 # We need to turn off this if we use pdb since we should stop at

411 # the first failure.

412 if config.getvalue("usepdb"):

413 continue_on_failure = False

414 return continue_on_failure

415

416

417 class DoctestTextfile(Module):

418 obj = None

419

420 def collect(self) -> Iterable[DoctestItem]:

421 import doctest

422

423 # Inspired by doctest.testfile; ideally we would use it directly,

424 # but it doesn't support passing a custom checker.

425 encoding = self.config.getini("doctest_encoding")

426 text = self.path.read_text(encoding)

427 filename = str(self.path)

428 name = self.path.name

429 globs = {"__name__": "__main__"}

430

431 optionflags = get_optionflags(self)

432

433 runner = _get_runner(

434 verbose=False,

435 optionflags=optionflags,

436 checker=_get_checker(),

437 continue_on_failure=_get_continue_on_failure(self.config),

438 )

439

440 parser = doctest.DocTestParser()

441 test = parser.get_doctest(text, globs, name, filename, 0)

442 if test.examples:

443 yield DoctestItem.from_parent(

444 self, name=test.name, runner=runner, dtest=test

445 )

446

447

448 def _check_all_skipped(test: "doctest.DocTest") -> None:

449 """Raise pytest.skip() if all examples in the given DocTest have the SKIP

450 option set."""

451 import doctest

452

453 all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples)

454 if all_skipped:

455 skip("all tests skipped by +SKIP option")

456

457

458 def _is_mocked(obj: object) -> bool:

459 """Return if an object is possibly a mock object by checking the

460 existence of a highly improbable attribute."""

461 return (

462 safe_getattr(obj, "pytest_mock_example_attribute_that_shouldnt_exist", None)

463 is not None

464 )

465

466

467@contextmanager

468 def _patch_unwrap_mock_aware() -> Generator[None, None, None]:

469 """Context manager which replaces ``inspect.unwrap`` with a version

470 that's aware of mock objects and doesn't recurse into them."""

471 real_unwrap = inspect.unwrap

472

473 def _mock_aware_unwrap(

474 func: Callable[..., Any], *, stop: Optional[Callable[[Any], Any]] = None

475 ) -> Any:

476 try:

477 if stop is None or stop is _is_mocked:

478 return real_unwrap(func, stop=_is_mocked)

479 _stop = stop

480 return real_unwrap(func, stop=lambda obj: _is_mocked(obj) or _stop(func))

481 except Exception as e:

482 warnings.warn(

483 "Got %r when unwrapping %r. This is usually caused "

484 "by a violation of Python's object protocol; see e.g. "

485 "https://github.com/pytest-dev/pytest/issues/5080" % (e, func),

486 PytestWarning,

487 )

488 raise

489

490 inspect.unwrap = _mock_aware_unwrap

491 try:

492 yield

493 finally:

494 inspect.unwrap = real_unwrap

495

496

497 class DoctestModule(Module):

498 def collect(self) -> Iterable[DoctestItem]:

499 import doctest

500

501 class MockAwareDocTestFinder(doctest.DocTestFinder):

502 """A hackish doctest finder that overrides stdlib internals to fix a stdlib bug.

503

504 https://github.com/pytest-dev/pytest/issues/3456

505 https://bugs.python.org/issue25532

506 """

507

508 def _find_lineno(self, obj, source_lines):

509 """Doctest code does not take into account `@property`, this

510 is a hackish way to fix it. https://bugs.python.org/issue17446

511

512 Wrapped Doctests will need to be unwrapped so the correct

513 line number is returned. This will be reported upstream. #8796

514 """

515 if isinstance(obj, property):

516 obj = getattr(obj, "fget", obj)

517

518 if hasattr(obj, "__wrapped__"):

519 # Get the main obj in case of it being wrapped

520 obj = inspect.unwrap(obj)

521

522 # Type ignored because this is a private function.

523 return super()._find_lineno( # type:ignore[misc]

524 obj,

525 source_lines,

526 )

527

528 def _find(

529 self, tests, obj, name, module, source_lines, globs, seen

530 ) -> None:

531 if _is_mocked(obj):

532 return

533 with _patch_unwrap_mock_aware():

534

535 # Type ignored because this is a private function.

536 super()._find( # type:ignore[misc]

537 tests, obj, name, module, source_lines, globs, seen

538 )

539

540 if self.path.name == "conftest.py":

541 module = self.config.pluginmanager._importconftest(

542 self.path,

543 self.config.getoption("importmode"),

544 rootpath=self.config.rootpath,

545 )

546 else:

547 try:

548 module = import_path(

549 self.path,

550 root=self.config.rootpath,

551 mode=self.config.getoption("importmode"),

552 )

553 except ImportError:

554 if self.config.getvalue("doctest_ignore_import_errors"):

555 skip("unable to import module %r" % self.path)

556 else:

557 raise

558 # Uses internal doctest module parsing mechanism.

559 finder = MockAwareDocTestFinder()

560 optionflags = get_optionflags(self)

561 runner = _get_runner(

562 verbose=False,

563 optionflags=optionflags,

564 checker=_get_checker(),

565 continue_on_failure=_get_continue_on_failure(self.config),

566 )

567

568 for test in finder.find(module, module.__name__):

569 if test.examples: # skip empty doctests

570 yield DoctestItem.from_parent(

571 self, name=test.name, runner=runner, dtest=test

572 )

573

574

575 def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:

576 """Used by DoctestTextfile and DoctestItem to setup fixture information."""

577

578 def func() -> None:

579 pass

580

581 doctest_item.funcargs = {} # type: ignore[attr-defined]

582 fm = doctest_item.session._fixturemanager

583 doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]

584 node=doctest_item, func=func, cls=None, funcargs=False

585 )

586 fixture_request = FixtureRequest(doctest_item, _ispytest=True)

587 fixture_request._fillfixtures()

588 return fixture_request

589

590

591 def _init_checker_class() -> Type["doctest.OutputChecker"]:

592 import doctest

593 import re

594

595 class LiteralsOutputChecker(doctest.OutputChecker):

596 # Based on doctest_nose_plugin.py from the nltk project

597 # (https://github.com/nltk/nltk) and on the "numtest" doctest extension

598 # by Sebastien Boisgerault (https://github.com/boisgera/numtest).

599

600 _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)

601 _bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE)

602 _number_re = re.compile(

603 r"""

604 (?P<number>

605 (?P<mantissa>

606 (?P<integer1> [+-]?\d*)\.(?P<fraction>\d+)

607 |

608 (?P<integer2> [+-]?\d+)\.

609 )

610 (?:

611 [Ee]

612 (?P<exponent1> [+-]?\d+)

613 )?

614 |

615 (?P<integer3> [+-]?\d+)

616 (?:

617 [Ee]

618 (?P<exponent2> [+-]?\d+)

619 )

620 )

621 """,

622 re.VERBOSE,

623 )

624

625 def check_output(self, want: str, got: str, optionflags: int) -> bool:

626 if super().check_output(want, got, optionflags):

627 return True

628

629 allow_unicode = optionflags & _get_allow_unicode_flag()

630 allow_bytes = optionflags & _get_allow_bytes_flag()

631 allow_number = optionflags & _get_number_flag()

632

633 if not allow_unicode and not allow_bytes and not allow_number:

634 return False

635

636 def remove_prefixes(regex: Pattern[str], txt: str) -> str:

637 return re.sub(regex, r"\1\2", txt)

638

639 if allow_unicode:

640 want = remove_prefixes(self._unicode_literal_re, want)

641 got = remove_prefixes(self._unicode_literal_re, got)

642

643 if allow_bytes:

644 want = remove_prefixes(self._bytes_literal_re, want)

645 got = remove_prefixes(self._bytes_literal_re, got)

646

647 if allow_number:

648 got = self._remove_unwanted_precision(want, got)

649

650 return super().check_output(want, got, optionflags)

651

652 def _remove_unwanted_precision(self, want: str, got: str) -> str:

653 wants = list(self._number_re.finditer(want))

654 gots = list(self._number_re.finditer(got))

655 if len(wants) != len(gots):

656 return got

657 offset = 0

658 for w, g in zip(wants, gots):

659 fraction: Optional[str] = w.group("fraction")

660 exponent: Optional[str] = w.group("exponent1")

661 if exponent is None:

662 exponent = w.group("exponent2")

663 precision = 0 if fraction is None else len(fraction)

664 if exponent is not None:

665 precision -= int(exponent)

666 if float(w.group()) == approx(float(g.group()), abs=10**-precision):

667 # They're close enough. Replace the text we actually

668 # got with the text we want, so that it will match when we

669 # check the string literally.

670 got = (

671 got[: g.start() + offset] + w.group() + got[g.end() + offset :]

672 )

673 offset += w.end() - w.start() - (g.end() - g.start())

674 return got

675

676 return LiteralsOutputChecker

677

678

679 def _get_checker() -> "doctest.OutputChecker":

680 """Return a doctest.OutputChecker subclass that supports some

681 additional options:

682

683 * ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b''

684 prefixes (respectively) in string literals. Useful when the same

685 doctest should run in Python 2 and Python 3.

686

687 * NUMBER to ignore floating-point differences smaller than the

688 precision of the literal number in the doctest.

689

690 An inner class is used to avoid importing "doctest" at the module

691 level.

692 """

693 global CHECKER_CLASS

694 if CHECKER_CLASS is None:

695 CHECKER_CLASS = _init_checker_class()

696 return CHECKER_CLASS()

697

698

699 def _get_allow_unicode_flag() -> int:

700 """Register and return the ALLOW_UNICODE flag."""

701 import doctest

702

703 return doctest.register_optionflag("ALLOW_UNICODE")

704

705

706 def _get_allow_bytes_flag() -> int:

707 """Register and return the ALLOW_BYTES flag."""

708 import doctest

709

710 return doctest.register_optionflag("ALLOW_BYTES")

711

712

713 def _get_number_flag() -> int:

714 """Register and return the NUMBER flag."""

715 import doctest

716

717 return doctest.register_optionflag("NUMBER")

718

719

720 def _get_report_choice(key: str) -> int:

721 """Return the actual `doctest` module flag value.

722

723 We want to do it as late as possible to avoid importing `doctest` and all

724 its dependencies when parsing options, as it adds overhead and breaks tests.

725 """

726 import doctest

727

728 return {

729 DOCTEST_REPORT_CHOICE_UDIFF: doctest.REPORT_UDIFF,

730 DOCTEST_REPORT_CHOICE_CDIFF: doctest.REPORT_CDIFF,

731 DOCTEST_REPORT_CHOICE_NDIFF: doctest.REPORT_NDIFF,

732 DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE: doctest.REPORT_ONLY_FIRST_FAILURE,

733 DOCTEST_REPORT_CHOICE_NONE: 0,

734 }[key]

735

736

737@fixture(scope="session")

738 def doctest_namespace() -> Dict[str, Any]:

739 """Fixture that returns a :py:class:`dict` that will be injected into the

740 namespace of doctests.

741

742 Usually this fixture is used in conjunction with another ``autouse`` fixture:

743

744 .. code-block:: python

745

746 @pytest.fixture(autouse=True)

747 def add_np(doctest_namespace):

748 doctest_namespace["np"] = numpy

749

750 For more details: :ref:`doctest_namespace`.

751 """

752 return dict()