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