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/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
+