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