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