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