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