scipy-doctest 1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scipy_doctest/__init__.py +12 -0
- scipy_doctest/__main__.py +6 -0
- scipy_doctest/conftest.py +5 -0
- scipy_doctest/frontend.py +468 -0
- scipy_doctest/impl.py +506 -0
- scipy_doctest/plugin.py +336 -0
- scipy_doctest/tests/__init__.py +0 -0
- scipy_doctest/tests/failure_cases.py +17 -0
- scipy_doctest/tests/failure_cases_2.py +27 -0
- scipy_doctest/tests/finder_cases.py +65 -0
- scipy_doctest/tests/local_file.txt +0 -0
- scipy_doctest/tests/local_file_cases.py +37 -0
- scipy_doctest/tests/module_cases.py +192 -0
- scipy_doctest/tests/octave_a.mat +0 -0
- scipy_doctest/tests/scipy_ndimage_tutorial_clone.rst +2018 -0
- scipy_doctest/tests/stopwords_cases.py +9 -0
- scipy_doctest/tests/test_finder.py +170 -0
- scipy_doctest/tests/test_parser.py +36 -0
- scipy_doctest/tests/test_pytest_configuration.py +95 -0
- scipy_doctest/tests/test_runner.py +105 -0
- scipy_doctest/tests/test_skipmarkers.py +202 -0
- scipy_doctest/tests/test_testfile.py +24 -0
- scipy_doctest/tests/test_testmod.py +158 -0
- scipy_doctest/util.py +279 -0
- scipy_doctest-1.1.dist-info/LICENSE +29 -0
- scipy_doctest-1.1.dist-info/METADATA +390 -0
- scipy_doctest-1.1.dist-info/RECORD +29 -0
- scipy_doctest-1.1.dist-info/WHEEL +4 -0
- scipy_doctest-1.1.dist-info/entry_points.txt +3 -0
scipy_doctest/impl.py
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import warnings
|
|
3
|
+
import doctest
|
|
4
|
+
from doctest import NORMALIZE_WHITESPACE, ELLIPSIS, IGNORE_EXCEPTION_DETAIL
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from . import util
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## shim numpy 1.x vs 2.0
|
|
12
|
+
if np.__version__ < "2":
|
|
13
|
+
VisibleDeprecationWarning = np.VisibleDeprecationWarning
|
|
14
|
+
else:
|
|
15
|
+
VisibleDeprecationWarning = np.exceptions.VisibleDeprecationWarning
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Register the optionflag to skip whole blocks, i.e.
|
|
19
|
+
# sequences of Examples without an intervening text.
|
|
20
|
+
SKIPBLOCK = doctest.register_optionflag('SKIPBLOCK')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DTConfig:
|
|
24
|
+
"""A bag class to collect various configuration bits.
|
|
25
|
+
|
|
26
|
+
If an attribute is None, helpful defaults are subsituted. If defaults
|
|
27
|
+
are not sufficient, users should create an instance of this class,
|
|
28
|
+
override the desired attributes and pass the instance to `testmod`.
|
|
29
|
+
|
|
30
|
+
Attributes
|
|
31
|
+
----------
|
|
32
|
+
default_namespace : dict
|
|
33
|
+
The namespace to run examples in.
|
|
34
|
+
check_namespace : dict
|
|
35
|
+
The namespace to do checks in.
|
|
36
|
+
rndm_markers : set
|
|
37
|
+
Additional directives which act like `# doctest: + SKIP`.
|
|
38
|
+
atol : float
|
|
39
|
+
rtol : float
|
|
40
|
+
Absolute and relative tolerances to check doctest examples with.
|
|
41
|
+
Specifically, the check is ``np.allclose(want, got, atol=atol, rtol=rtol)``
|
|
42
|
+
optionflags : int
|
|
43
|
+
doctest optionflags
|
|
44
|
+
Default is ``NORMALIZE_WHITESPACE | ELLIPSIS | IGNORE_EXCEPTION_DETAIL``
|
|
45
|
+
stopwords : set
|
|
46
|
+
If an example contains any of these stopwords, do not check the output
|
|
47
|
+
(but do check that the source is valid python).
|
|
48
|
+
pseudocode : list
|
|
49
|
+
List of strings. If an example contains any of these substrings, it
|
|
50
|
+
is not doctested at all. This is similar to the ``# doctest +SKIP``
|
|
51
|
+
directive. Typical candidates for this list are pseudocode blocks
|
|
52
|
+
``>>> from example import some_function`` or some such.
|
|
53
|
+
skiplist : set
|
|
54
|
+
A list of names of objects whose docstrings are known to fail doctesting
|
|
55
|
+
and we like to keep it that way.
|
|
56
|
+
user_context_mgr
|
|
57
|
+
A context manager to run tests in. Is entered for each DocTest
|
|
58
|
+
(for API docs, this is typically a single docstring). The operation is
|
|
59
|
+
roughly
|
|
60
|
+
|
|
61
|
+
>>> for test in tests:
|
|
62
|
+
... with user_context(test):
|
|
63
|
+
... runner.run(test)
|
|
64
|
+
Default is a noop.
|
|
65
|
+
local_resources: dict
|
|
66
|
+
If a test needs some local files, list them here. The format is
|
|
67
|
+
``{test.name : list-of-files}``
|
|
68
|
+
File paths are relative to path of ``test.filename``.
|
|
69
|
+
parse_namedtuples : bool
|
|
70
|
+
Whether to compare e.g. ``TTestResult(pvalue=0.9, statistic=42)``
|
|
71
|
+
literally or extract the numbers and compare the tuple ``(0.9, 42)``.
|
|
72
|
+
Default is True.
|
|
73
|
+
nameerror_after_exception : bool
|
|
74
|
+
If an example fails, next examples in the same test may raise spurious
|
|
75
|
+
NameErrors. Set to True if you want to see these, or if your test
|
|
76
|
+
is actually expected to raise NameErrors.
|
|
77
|
+
Default is False.
|
|
78
|
+
pytest_extra_ignore : list
|
|
79
|
+
A list of names/modules to ignore when run under pytest plugin. This is
|
|
80
|
+
equivalent to using `--ignore=...` cmdline switch.
|
|
81
|
+
pytest_extra_skip : dict
|
|
82
|
+
Names/modules to skip when run under pytest plugin. This is
|
|
83
|
+
equivalent to decorating the doctest with `@pytest.mark.skip` or adding
|
|
84
|
+
`# doctest: + SKIP` to its examples.
|
|
85
|
+
Each key is a doctest name to skip, and the corresponding value is
|
|
86
|
+
a string. If not empty, the string value is used as the skip reason.
|
|
87
|
+
pytest_extra_xfail : dict
|
|
88
|
+
Names/modules to xfail when run under pytest plugin. This is
|
|
89
|
+
equivalent to decorating the doctest with `@pytest.mark.xfail` or
|
|
90
|
+
adding `# may vary` to the outputs of all examples.
|
|
91
|
+
Each key is a doctest name to skip, and the corresponding value is
|
|
92
|
+
a string. If not empty, the string value is used as the skip reason.
|
|
93
|
+
"""
|
|
94
|
+
def __init__(self, *, # DTChecker configuration
|
|
95
|
+
CheckerKlass=None,
|
|
96
|
+
default_namespace=None,
|
|
97
|
+
check_namespace=None,
|
|
98
|
+
rndm_markers=None,
|
|
99
|
+
atol=1e-8,
|
|
100
|
+
rtol=1e-2,
|
|
101
|
+
# DTRunner configuration
|
|
102
|
+
optionflags=None,
|
|
103
|
+
# DTFinder/DTParser configuration
|
|
104
|
+
stopwords=None,
|
|
105
|
+
pseudocode=None,
|
|
106
|
+
skiplist=None,
|
|
107
|
+
# Additional user configuration
|
|
108
|
+
user_context_mgr=None,
|
|
109
|
+
local_resources=None,
|
|
110
|
+
# Obscure switches
|
|
111
|
+
parse_namedtuples=True, # Checker
|
|
112
|
+
nameerror_after_exception=False, # Runner
|
|
113
|
+
# plugin
|
|
114
|
+
pytest_extra_ignore=None,
|
|
115
|
+
pytest_extra_skip=None,
|
|
116
|
+
pytest_extra_xfail=None,
|
|
117
|
+
):
|
|
118
|
+
### DTChecker configuration ###
|
|
119
|
+
self.CheckerKlass = CheckerKlass or DTChecker
|
|
120
|
+
|
|
121
|
+
# The namespace to run examples in
|
|
122
|
+
self.default_namespace = default_namespace or {}
|
|
123
|
+
|
|
124
|
+
# The namespace to do checks in
|
|
125
|
+
if check_namespace is None:
|
|
126
|
+
check_namespace = {
|
|
127
|
+
'np': np,
|
|
128
|
+
'assert_allclose': np.testing.assert_allclose,
|
|
129
|
+
'assert_equal': np.testing.assert_equal,
|
|
130
|
+
# recognize numpy repr's
|
|
131
|
+
'array': np.array,
|
|
132
|
+
'matrix': np.matrix,
|
|
133
|
+
'masked_array': np.ma.masked_array,
|
|
134
|
+
'int64': np.int64,
|
|
135
|
+
'uint64': np.uint64,
|
|
136
|
+
'int8': np.int8,
|
|
137
|
+
'int32': np.int32,
|
|
138
|
+
'float32': np.float32,
|
|
139
|
+
'float64': np.float64,
|
|
140
|
+
'dtype': np.dtype,
|
|
141
|
+
'nan': np.nan,
|
|
142
|
+
'nanj': np.complex128(1j*np.nan),
|
|
143
|
+
'infj': complex(0, np.inf),
|
|
144
|
+
'NaN': np.nan,
|
|
145
|
+
'inf': np.inf,
|
|
146
|
+
'Inf': np.inf, }
|
|
147
|
+
self.check_namespace = check_namespace
|
|
148
|
+
|
|
149
|
+
# Additional directives which act like `# doctest: + SKIP`
|
|
150
|
+
if rndm_markers is None:
|
|
151
|
+
rndm_markers = {'# random', '# Random',
|
|
152
|
+
'#random', '#Random',
|
|
153
|
+
"# may vary"}
|
|
154
|
+
self.rndm_markers = rndm_markers
|
|
155
|
+
|
|
156
|
+
self.atol, self.rtol = atol, rtol
|
|
157
|
+
|
|
158
|
+
### DTRunner configuration ###
|
|
159
|
+
|
|
160
|
+
# doctest optionflags
|
|
161
|
+
if optionflags is None:
|
|
162
|
+
optionflags = NORMALIZE_WHITESPACE | ELLIPSIS | IGNORE_EXCEPTION_DETAIL
|
|
163
|
+
self.optionflags = optionflags
|
|
164
|
+
|
|
165
|
+
### DTFinder/DTParser configuration ###
|
|
166
|
+
# ignore examples which contain any of these stopwords
|
|
167
|
+
if stopwords is None:
|
|
168
|
+
stopwords = {'plt.', '.hist', '.show', '.ylim', '.subplot(',
|
|
169
|
+
'set_title', 'imshow', 'plt.show', '.axis(', '.plot(',
|
|
170
|
+
'.bar(', '.title', '.ylabel', '.xlabel', 'set_ylim', 'set_xlim',
|
|
171
|
+
'# reformatted', '.set_xlabel(', '.set_ylabel(', '.set_zlabel(',
|
|
172
|
+
'.set(xlim=', '.set(ylim=', '.set(xlabel=', '.set(ylabel=', '.xlim(',
|
|
173
|
+
'ax.set('}
|
|
174
|
+
self.stopwords = stopwords
|
|
175
|
+
|
|
176
|
+
if pseudocode is None:
|
|
177
|
+
pseudocode = set()
|
|
178
|
+
self.pseudocode = pseudocode
|
|
179
|
+
|
|
180
|
+
# these names are known to fail doctesting and we like to keep it that way
|
|
181
|
+
# e.g. sometimes pseudocode is acceptable etc
|
|
182
|
+
if skiplist is None:
|
|
183
|
+
skiplist = set(['scipy.special.sinc', # comes from numpy
|
|
184
|
+
'scipy.misc.who', # comes from numpy
|
|
185
|
+
'scipy.optimize.show_options', ])
|
|
186
|
+
self.skiplist = skiplist
|
|
187
|
+
|
|
188
|
+
#### User configuration
|
|
189
|
+
if user_context_mgr is None:
|
|
190
|
+
user_context_mgr = util.noop_context_mgr
|
|
191
|
+
self.user_context_mgr = user_context_mgr
|
|
192
|
+
|
|
193
|
+
#### Local resources: None or dict {test: list-of-files-to-copy}
|
|
194
|
+
self.local_resources=local_resources or dict()
|
|
195
|
+
|
|
196
|
+
#### Obscure switches, best leave intact
|
|
197
|
+
self.parse_namedtuples = parse_namedtuples
|
|
198
|
+
self.nameerror_after_exception = nameerror_after_exception
|
|
199
|
+
|
|
200
|
+
#### pytest plugin additional switches
|
|
201
|
+
self.pytest_extra_ignore = pytest_extra_ignore or []
|
|
202
|
+
self.pytest_extra_skip = pytest_extra_skip or {}
|
|
203
|
+
self.pytest_extra_xfail = pytest_extra_xfail or {}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def try_convert_namedtuple(got):
|
|
207
|
+
# suppose that "got" is smth like MoodResult(statistic=10, pvalue=0.1).
|
|
208
|
+
# Then convert it to the tuple (10, 0.1), so that can later compare tuples.
|
|
209
|
+
num = got.count('=')
|
|
210
|
+
if num == 0:
|
|
211
|
+
# not a nameduple, bail out
|
|
212
|
+
return got
|
|
213
|
+
regex = (r'[\w\d_]+\(' +
|
|
214
|
+
', '.join([r'[\w\d_]+=(.+)']*num) +
|
|
215
|
+
r'\)')
|
|
216
|
+
grp = re.findall(regex, " ".join(got.split()))
|
|
217
|
+
# fold it back to a tuple
|
|
218
|
+
got_again = '(' + ', '.join(grp[0]) + ')'
|
|
219
|
+
return got_again
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def has_masked(got):
|
|
223
|
+
return 'masked_array' in got and '--' in got
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class DTChecker(doctest.OutputChecker):
|
|
227
|
+
obj_pattern = re.compile(r'at 0x[0-9a-fA-F]+>')
|
|
228
|
+
vanilla = doctest.OutputChecker()
|
|
229
|
+
|
|
230
|
+
def __init__(self, config=None):
|
|
231
|
+
if config is None:
|
|
232
|
+
config = DTConfig()
|
|
233
|
+
self.config = config
|
|
234
|
+
|
|
235
|
+
self.atol, self.rtol = self.config.atol, self.config.rtol
|
|
236
|
+
self.rndm_markers = set(self.config.rndm_markers)
|
|
237
|
+
self.rndm_markers.add('# _ignore') # technical, private. See DTParser
|
|
238
|
+
|
|
239
|
+
def check_output(self, want, got, optionflags):
|
|
240
|
+
|
|
241
|
+
# cut it short if they are equal
|
|
242
|
+
if want == got:
|
|
243
|
+
return True
|
|
244
|
+
|
|
245
|
+
# skip random stuff
|
|
246
|
+
if any(word in want for word in self.rndm_markers):
|
|
247
|
+
return True
|
|
248
|
+
|
|
249
|
+
# skip function/object addresses
|
|
250
|
+
if self.obj_pattern.search(got):
|
|
251
|
+
return True
|
|
252
|
+
|
|
253
|
+
# ignore comments (e.g. signal.freqresp)
|
|
254
|
+
if want.lstrip().startswith("#"):
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
# try the standard doctest
|
|
258
|
+
try:
|
|
259
|
+
if self.vanilla.check_output(want, got, optionflags):
|
|
260
|
+
return True
|
|
261
|
+
except Exception:
|
|
262
|
+
pass
|
|
263
|
+
|
|
264
|
+
# OK then, convert strings to objects
|
|
265
|
+
ns = dict(self.config.check_namespace)
|
|
266
|
+
try:
|
|
267
|
+
with warnings.catch_warnings():
|
|
268
|
+
# NumPy's ragged array deprecation of np.array([1, (2, 3)]);
|
|
269
|
+
# also array abbreviations: try `np.diag(np.arange(1000))`
|
|
270
|
+
warnings.simplefilter('ignore', VisibleDeprecationWarning)
|
|
271
|
+
|
|
272
|
+
a_want = eval(want, dict(ns))
|
|
273
|
+
a_got = eval(got, dict(ns))
|
|
274
|
+
except Exception:
|
|
275
|
+
# Maybe we're printing a numpy array? This produces invalid python
|
|
276
|
+
# code: `print(np.arange(3))` produces "[0 1 2]" w/o commas between
|
|
277
|
+
# values. So, reinsert commas and retry.
|
|
278
|
+
s_want = want.strip()
|
|
279
|
+
s_got = got.strip()
|
|
280
|
+
cond = (s_want.startswith("[") and s_want.endswith("]") and
|
|
281
|
+
s_got.startswith("[") and s_got.endswith("]"))
|
|
282
|
+
if cond:
|
|
283
|
+
s_want = ", ".join(s_want[1:-1].split())
|
|
284
|
+
s_got = ", ".join(s_got[1:-1].split())
|
|
285
|
+
return self.check_output(s_want, s_got, optionflags)
|
|
286
|
+
|
|
287
|
+
#handle array abbreviation for n-dimensional arrays, n >= 1
|
|
288
|
+
ndim_array = (s_want.startswith("array([") and s_want.endswith("])") and
|
|
289
|
+
s_got.startswith("array([") and s_got.endswith("])"))
|
|
290
|
+
if ndim_array:
|
|
291
|
+
s_want = ''.join(s_want.split('...,'))
|
|
292
|
+
s_got = ''.join(s_got.split('...,'))
|
|
293
|
+
return self.check_output(s_want, s_got, optionflags)
|
|
294
|
+
|
|
295
|
+
# maybe we are dealing with masked arrays?
|
|
296
|
+
# their repr uses '--' for masked values and this is invalid syntax
|
|
297
|
+
# If so, replace '--' by nans (they are masked anyway) and retry
|
|
298
|
+
if has_masked(want) or has_masked(got):
|
|
299
|
+
s_want = want.replace('--', 'nan')
|
|
300
|
+
s_got = got.replace('--', 'nan')
|
|
301
|
+
return self.check_output(s_want, s_got, optionflags)
|
|
302
|
+
|
|
303
|
+
if "=" not in want and "=" not in got:
|
|
304
|
+
# if we're here, want and got cannot be eval-ed (hence cannot
|
|
305
|
+
# be converted to numpy objects), they are not namedtuples
|
|
306
|
+
# (those must have at least one '=' sign).
|
|
307
|
+
# Thus they should have compared equal with vanilla doctest.
|
|
308
|
+
# Since they did not, it's an error.
|
|
309
|
+
return False
|
|
310
|
+
|
|
311
|
+
if not self.config.parse_namedtuples:
|
|
312
|
+
return False
|
|
313
|
+
# suppose that "want" is a tuple, and "got" is smth like
|
|
314
|
+
# MoodResult(statistic=10, pvalue=0.1).
|
|
315
|
+
# Then convert the latter to the tuple (10, 0.1),
|
|
316
|
+
# and then compare the tuples.
|
|
317
|
+
try:
|
|
318
|
+
got_again = try_convert_namedtuple(got)
|
|
319
|
+
want_again = try_convert_namedtuple(want)
|
|
320
|
+
except Exception:
|
|
321
|
+
return False
|
|
322
|
+
else:
|
|
323
|
+
return self.check_output(want_again, got_again, optionflags)
|
|
324
|
+
|
|
325
|
+
# ... and defer to numpy
|
|
326
|
+
try:
|
|
327
|
+
return self._do_check(a_want, a_got)
|
|
328
|
+
except Exception:
|
|
329
|
+
# heterog tuple, eg (1, np.array([1., 2.]))
|
|
330
|
+
try:
|
|
331
|
+
return all(self._do_check(w, g) for w, g in zip(a_want, a_got))
|
|
332
|
+
except (TypeError, ValueError):
|
|
333
|
+
return False
|
|
334
|
+
|
|
335
|
+
def _do_check(self, want, got):
|
|
336
|
+
# This should be done exactly as written to correctly handle all of
|
|
337
|
+
# numpy-comparable objects, strings, and heterogeneous tuples
|
|
338
|
+
try:
|
|
339
|
+
if want == got:
|
|
340
|
+
return True
|
|
341
|
+
except Exception:
|
|
342
|
+
pass
|
|
343
|
+
with warnings.catch_warnings():
|
|
344
|
+
# NumPy's ragged array deprecation of np.array([1, (2, 3)])
|
|
345
|
+
warnings.simplefilter('ignore', VisibleDeprecationWarning)
|
|
346
|
+
|
|
347
|
+
# This line is the crux of the whole thing. The rest is mostly scaffolding.
|
|
348
|
+
result = np.allclose(want, got, atol=self.atol, rtol=self.rtol, equal_nan=True)
|
|
349
|
+
return result
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class DTRunner(doctest.DocTestRunner):
|
|
353
|
+
DIVIDER = "\n"
|
|
354
|
+
|
|
355
|
+
def __init__(self, checker=None, verbose=None, optionflags=None, config=None):
|
|
356
|
+
if config is None:
|
|
357
|
+
config = DTConfig()
|
|
358
|
+
if checker is None:
|
|
359
|
+
checker = config.CheckerKlass(config)
|
|
360
|
+
self.nameerror_after_exception = config.nameerror_after_exception
|
|
361
|
+
if optionflags is None:
|
|
362
|
+
optionflags = config.optionflags
|
|
363
|
+
super().__init__(checker=checker, verbose=verbose, optionflags=optionflags)
|
|
364
|
+
|
|
365
|
+
def _report_item_name(self, out, item_name, new_line=False):
|
|
366
|
+
if item_name is not None:
|
|
367
|
+
out("\n " + item_name + "\n " + "-"*len(item_name))
|
|
368
|
+
if new_line:
|
|
369
|
+
out("\n")
|
|
370
|
+
|
|
371
|
+
def report_start(self, out, test, example):
|
|
372
|
+
return super().report_start(out, test, example)
|
|
373
|
+
|
|
374
|
+
def report_success(self, out, test, example, got):
|
|
375
|
+
if self._verbose:
|
|
376
|
+
self._report_item_name(out, test.name, new_line=True)
|
|
377
|
+
return super().report_success(out, test, example, got)
|
|
378
|
+
|
|
379
|
+
def report_unexpected_exception(self, out, test, example, exc_info):
|
|
380
|
+
if not self.nameerror_after_exception:
|
|
381
|
+
# Ignore name errors after failing due to an unexpected exception
|
|
382
|
+
# NB: this came in in https://github.com/scipy/scipy/pull/13116
|
|
383
|
+
# However, here we attach the flag to the test itself, not the runner
|
|
384
|
+
if not hasattr(test, 'had_unexpected_error'):
|
|
385
|
+
test.had_unexpected_error = True
|
|
386
|
+
else:
|
|
387
|
+
exception_type = exc_info[0]
|
|
388
|
+
if exception_type is NameError:
|
|
389
|
+
return
|
|
390
|
+
self._report_item_name(out, test.name)
|
|
391
|
+
return super().report_unexpected_exception(out, test, example, exc_info)
|
|
392
|
+
|
|
393
|
+
def report_failure(self, out, test, example, got):
|
|
394
|
+
self._report_item_name(out, test.name)
|
|
395
|
+
return super().report_failure(out, test, example, got)
|
|
396
|
+
|
|
397
|
+
def get_history(self):
|
|
398
|
+
"""Return a dict with names of items which were run.
|
|
399
|
+
|
|
400
|
+
Actually the dict is `{name : (f, t)}`, where `name` is the name of
|
|
401
|
+
an object, and the value is a tuple of the numbers of examples which
|
|
402
|
+
failed and which were tried.
|
|
403
|
+
"""
|
|
404
|
+
return self._name2ft
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class DebugDTRunner(DTRunner):
|
|
408
|
+
"""Doctest runner which raises on a first error.
|
|
409
|
+
|
|
410
|
+
Almost verbatim copy of `doctest.DebugRunner`.
|
|
411
|
+
"""
|
|
412
|
+
def run(self, test, compileflags=None, out=None, clear_globs=True):
|
|
413
|
+
r = super().run(
|
|
414
|
+
test, compileflags=compileflags, out=out, clear_globs=clear_globs
|
|
415
|
+
)
|
|
416
|
+
if clear_globs:
|
|
417
|
+
test.globs.clear()
|
|
418
|
+
return r
|
|
419
|
+
|
|
420
|
+
def report_unexpected_exception(self, out, test, example, exc_info):
|
|
421
|
+
super().report_unexpected_exception(out, test, example, exc_info)
|
|
422
|
+
out('\n')
|
|
423
|
+
raise doctest.UnexpectedException(test, example, exc_info)
|
|
424
|
+
|
|
425
|
+
def report_failure(self, out, test, example, got):
|
|
426
|
+
super().report_failure(out, test, example, got)
|
|
427
|
+
out('\n')
|
|
428
|
+
raise doctest.DocTestFailure(test, example, got)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
class DTFinder(doctest.DocTestFinder):
|
|
432
|
+
"""A Finder with helpful defaults.
|
|
433
|
+
"""
|
|
434
|
+
def __init__(self, verbose=None, parser=None, recurse=True,
|
|
435
|
+
exclude_empty=True, config=None):
|
|
436
|
+
if config is None:
|
|
437
|
+
config = DTConfig()
|
|
438
|
+
self.config = config
|
|
439
|
+
if parser is None:
|
|
440
|
+
parser = DTParser(config)
|
|
441
|
+
verbose, dtverbose = util._map_verbosity(verbose)
|
|
442
|
+
super().__init__(dtverbose, parser, recurse, exclude_empty)
|
|
443
|
+
|
|
444
|
+
def find(self, obj, name=None, module=None, globs=None, extraglobs=None):
|
|
445
|
+
if globs is None:
|
|
446
|
+
globs = dict(self.config.default_namespace)
|
|
447
|
+
# XXX: does this make similar checks in testmod/testfile duplicate?
|
|
448
|
+
if module not in self.config.skiplist:
|
|
449
|
+
tests = super().find(obj, name, module, globs, extraglobs)
|
|
450
|
+
return tests
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class DTParser(doctest.DocTestParser):
|
|
454
|
+
"""A Parser with a stopword list.
|
|
455
|
+
"""
|
|
456
|
+
def __init__(self, config=None):
|
|
457
|
+
if config is None:
|
|
458
|
+
config = DTConfig()
|
|
459
|
+
self.config = config
|
|
460
|
+
# DocTestParser has no __init__, do not try calling it
|
|
461
|
+
|
|
462
|
+
def get_examples(self, string, name='<string>'):
|
|
463
|
+
"""Get examples from intervening strings and examples.
|
|
464
|
+
|
|
465
|
+
How this works
|
|
466
|
+
--------------
|
|
467
|
+
This function is used (read the source!) in
|
|
468
|
+
`doctest.DocTestParser().get_doctests(...)`. Over there, `self.parse`
|
|
469
|
+
splits the input string into into a list of `Examples` and intervening
|
|
470
|
+
text strings. `get_examples` method selects `Example`s from this list.
|
|
471
|
+
Here we inject our logic for filtering out stopwords and pseudocode.
|
|
472
|
+
|
|
473
|
+
TODO: document the differences between stopwords, pseudocode and +SKIP.
|
|
474
|
+
"""
|
|
475
|
+
stopwords = self.config.stopwords
|
|
476
|
+
pseudocode = self.config.pseudocode
|
|
477
|
+
|
|
478
|
+
SKIP = doctest.OPTIONFLAGS_BY_NAME['SKIP']
|
|
479
|
+
keep_skipping_this_block = False
|
|
480
|
+
|
|
481
|
+
examples = []
|
|
482
|
+
for example in self.parse(string, name):
|
|
483
|
+
# .parse returns a list of examples and intervening text
|
|
484
|
+
if not isinstance(example, doctest.Example):
|
|
485
|
+
if example:
|
|
486
|
+
keep_skipping_this_block = False
|
|
487
|
+
continue
|
|
488
|
+
|
|
489
|
+
if SKIPBLOCK in example.options or keep_skipping_this_block:
|
|
490
|
+
# skip this one and continue skipping until there is
|
|
491
|
+
# a non-empty line of text (which signals the end of the block)
|
|
492
|
+
example.options[SKIP] = True
|
|
493
|
+
keep_skipping_this_block = True
|
|
494
|
+
|
|
495
|
+
if any(word in example.source for word in pseudocode):
|
|
496
|
+
# Found pseudocode. Add a `#doctest: +SKIP` directive.
|
|
497
|
+
# NB: Could have just skipped it via `continue`.
|
|
498
|
+
example.options[SKIP] = True
|
|
499
|
+
|
|
500
|
+
if any(word in example.source for word in stopwords):
|
|
501
|
+
# Found a stopword. Do not check the output (but do check
|
|
502
|
+
# that the source is valid python).
|
|
503
|
+
example.want += " # _ignore\n"
|
|
504
|
+
examples.append(example)
|
|
505
|
+
return examples
|
|
506
|
+
|