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