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