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