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.
@@ -0,0 +1,336 @@
1
+ """
2
+ A pytest plugin that provides enhanced doctesting for Pydata libraries
3
+ """
4
+ import bdb
5
+ import warnings
6
+ import doctest
7
+
8
+ import pytest
9
+ import _pytest
10
+ from _pytest import doctest as pydoctest, outcomes
11
+ from _pytest.doctest import DoctestModule, DoctestTextfile
12
+ from _pytest.pathlib import import_path
13
+
14
+ from .impl import DTParser, DebugDTRunner
15
+ from .conftest import dt_config
16
+ from .util import np_errstate, matplotlib_make_nongui, temp_cwd
17
+ from .frontend import find_doctests
18
+
19
+
20
+ def pytest_addoption(parser):
21
+ group = parser.getgroup("collect")
22
+
23
+ group.addoption(
24
+ "--doctest-collect",
25
+ action="store",
26
+ default="None",
27
+ help="Doctest collection strategy: vanilla pytest ('None', default), or 'api'",
28
+ choices=("None", "api"),
29
+ dest="collection_strategy"
30
+ )
31
+
32
+
33
+ def pytest_configure(config):
34
+ """
35
+ Perform initial configuration for the pytest plugin.
36
+ """
37
+
38
+ # Create a dt config attribute within pytest's config object for easy access.
39
+ config.dt_config = dt_config
40
+
41
+ # Override doctest's objects with the plugin's alternative implementation.
42
+ pydoctest.DoctestModule = DTModule
43
+ pydoctest.DoctestTextfile = DTTextfile
44
+
45
+
46
+ def pytest_ignore_collect(collection_path, config):
47
+ """
48
+ Determine whether to ignore the specified collection path.
49
+ This function is used to exclude the 'tests' directory and test modules when
50
+ the '--doctest-modules' option is used.
51
+ """
52
+ if config.getoption("--doctest-modules"):
53
+ path_str = str(collection_path)
54
+ if "tests" in path_str or "test_" in path_str:
55
+ return True
56
+
57
+ for entry in config.dt_config.pytest_extra_ignore:
58
+ if entry in str(collection_path):
59
+ return True
60
+
61
+
62
+ def is_private(item):
63
+ """Decide if an DocTestItem `item` is private.
64
+
65
+ Private items are ignored in pytest_collect_modifyitem`.
66
+ """
67
+ # Here we look at the name of a test module/object. A seemingly less
68
+ # hacky alternative is to populate a set of seen `item.dtest` attributes
69
+ # (which are actual DocTest objects). The issue with that is it's tricky
70
+ # for explicit skips/ignores. Do we skip linalg.det or linalg._basic.det?
71
+ # (collection order is not guaranteed)
72
+ parent_full_name = item.parent.module.__name__
73
+ is_private = "._" in parent_full_name
74
+ return is_private
75
+
76
+
77
+ def _maybe_add_markers(item, config):
78
+ """Add xfail/skip markers to `item` if DTConfig says so.
79
+
80
+ Modifies the item in-place.
81
+ """
82
+ dt_config = config.dt_config
83
+
84
+ extra_skip = dt_config.pytest_extra_skip
85
+ skip_it = item.name in extra_skip
86
+ if skip_it:
87
+ reason = extra_skip[item.name] or ''
88
+ item.add_marker(
89
+ pytest.mark.skip(reason=reason)
90
+ )
91
+
92
+ extra_xfail = dt_config.pytest_extra_xfail
93
+ fail_it = item.name in extra_xfail
94
+ if fail_it:
95
+ reason = extra_xfail[item.name] or ''
96
+ item.add_marker(
97
+ pytest.mark.xfail(reason=reason)
98
+ )
99
+
100
+
101
+ def pytest_collection_modifyitems(config, items):
102
+ """
103
+ This hook is executed after test collection and allows you to modify the list of collected items.
104
+
105
+ The function removes
106
+ - duplicate Doctest items (e.g., scipy.stats.norm and scipy.stats.distributions.norm)
107
+ - Doctest items from underscored or otherwise private modules (e.g., scipy.special._precompute)
108
+
109
+ Note that this functions cooperates with and cleans up after `DTModule.collect`, which does the
110
+ bulk of the collection work.
111
+ """
112
+ # XXX: The logic in this function can probably be folded into DTModule.collect.
113
+ # I (E.B.) quickly tried it and it does not seem to just work. Apparently something
114
+ # pytest-y runs in between DTModule.collect and this hook (should that something
115
+ # be the proper home for all collection?).
116
+ # Also note that DTTextfile needs _maybe_add_markers, too.
117
+
118
+ need_filter_unique = (
119
+ config.getoption("--doctest-modules") and
120
+ config.getvalue("collection_strategy") == 'api'
121
+ )
122
+
123
+ unique_items = []
124
+
125
+ for item in items:
126
+ if isinstance(item.parent, DTModule) and need_filter_unique:
127
+ # objects are collected twice: from their public module + from the impl module
128
+ # e.g. for `levy_stable` we have
129
+ # (Pdb) p item.name, item.parent.name
130
+ # ('scipy.stats.levy_stable', 'build-install/lib/python3.10/site-packages/scipy/stats/__init__.py')
131
+ # and
132
+ # ('scipy.stats.distributions.levy_stable', 'distributions.py')
133
+ # so we filter out the second occurence
134
+ #
135
+ # There are two options:
136
+ # - either the impl module has a leading underscore (scipy.linalg._basic), or
137
+ # - it needs to be explicitly listed in the 'extra_ignore' config key (distributions.py)
138
+ #
139
+ # Note that the last part cannot be automated: scipy.cluster.vq is public, but
140
+ # scipy.stats.distributions is not
141
+ extra_ignore = config.dt_config.pytest_extra_ignore
142
+ parent_full_name = item.parent.module.__name__
143
+ is_duplicate = parent_full_name in extra_ignore or item.name in extra_ignore
144
+
145
+ if is_duplicate or is_private(item):
146
+ # ignore it
147
+ continue
148
+
149
+ _maybe_add_markers(item, config)
150
+ unique_items.append(item)
151
+
152
+ # Replace the original list of test items with the unique ones
153
+ items[:] = unique_items
154
+
155
+
156
+ def _is_deprecated(module):
157
+ """Detect if a module is deprecated (i.e., raises or warns on getattr)."""
158
+ names = dir(module)
159
+ if not names:
160
+ return False
161
+
162
+ res = False
163
+ try:
164
+ with warnings.catch_warnings():
165
+ warnings.simplefilter('error', DeprecationWarning)
166
+ getattr(module, names[0])
167
+ res = False
168
+ except DeprecationWarning:
169
+ res = True
170
+
171
+ return res
172
+
173
+
174
+ class DTModule(DoctestModule):
175
+ """
176
+ This class extends the DoctestModule class provided by pytest.
177
+
178
+ DTModule is responsible for overriding the behavior of the collect method.
179
+ The collect method is called by pytest to collect and generate test items for doctests
180
+ in the specified module or file.
181
+ """
182
+ def collect(self):
183
+ if pytest.__version__ < '8':
184
+ # Part of this code is copy-pasted from the `_pytest.doctest` module(pytest 7.4.0):
185
+ # https://github.com/pytest-dev/pytest/blob/448563caaac559b8a3195edc58e8806aca8d2c71/src/_pytest/doctest.py#L497
186
+ if self.path.name == "setup.py":
187
+ return
188
+ if self.path.name == "conftest.py":
189
+ module = self.config.pluginmanager._importconftest(
190
+ self.path,
191
+ self.config.getoption("importmode"),
192
+ rootpath=self.config.rootpath
193
+ )
194
+ else:
195
+ try:
196
+ module = import_path(
197
+ self.path,
198
+ root=self.config.rootpath,
199
+ mode=self.config.getoption("importmode"),
200
+ )
201
+ except ImportError:
202
+ if self.config.getvalue("doctest_ignore_import_errors"):
203
+ outcomes.skip("unable to import module %r" % self.path)
204
+ else:
205
+ raise
206
+
207
+ # XXX: `assert module == self.obj` seems to work (so is it all automatic?)
208
+ # but what are failure modes
209
+ else:
210
+ # https://github.com/pytest-dev/pytest/blob/8.1.0/src/_pytest/doctest.py#L561
211
+ try:
212
+ module = self.obj
213
+ except _pytest.nodes.Collector.CollectError:
214
+ if self.config.getvalue("doctest_ignore_import_errors"):
215
+ outcomes.skip("unable to import module %r" % self.path)
216
+ else:
217
+ raise
218
+
219
+ if _is_deprecated(module):
220
+ # bail out early
221
+ return
222
+
223
+ optionflags = dt_config.optionflags
224
+
225
+ # Plug in the custom runner: `PytestDTRunner`
226
+ runner = _get_runner(self.config,
227
+ verbose=False,
228
+ optionflags=optionflags,
229
+ )
230
+
231
+ # strategy='api': discover doctests in public, non-deprecated objects in module
232
+ # strategy=None : use vanilla stdlib doctest discovery
233
+ strategy = self.config.getvalue("collection_strategy")
234
+ if strategy == 'None':
235
+ strategy = None
236
+
237
+ # NB: additional postprocessing in pytest_collection_modifyitems
238
+ for test in find_doctests(module, strategy=strategy, name=module.__name__, config=dt_config):
239
+ if test.examples: # skip empty doctests
240
+ yield pydoctest.DoctestItem.from_parent(
241
+ self, name=test.name, runner=runner, dtest=test
242
+ )
243
+
244
+
245
+ class DTTextfile(DoctestTextfile):
246
+ """
247
+ This class extends the DoctestTextfile class provided by pytest.
248
+
249
+ DTTextfile is responsible for overriding the behavior of the collect method.
250
+ The collect method is called by pytest to collect and generate test items for doctests
251
+ in the specified text files.
252
+ """
253
+ def collect(self):
254
+ # Part of this code is copy-pasted from `_pytest.doctest` module(pytest 7.4.0):
255
+ # https://github.com/pytest-dev/pytest/blob/448563caaac559b8a3195edc58e8806aca8d2c71/src/_pytest/doctest.py#L417
256
+ encoding = self.config.getini("doctest_encoding")
257
+ text = self.path.read_text(encoding)
258
+ filename = str(self.path)
259
+ name = self.path.name
260
+ globs = {"__name__": "__main__"}
261
+
262
+ optionflags = dt_config.optionflags
263
+
264
+ # Plug in the custom runner: `PytestDTRunner`
265
+ runner = _get_runner(self.config,
266
+ verbose=False,
267
+ optionflags=optionflags,
268
+ )
269
+
270
+ # Plug in an instance of `DTParser` which parses the doctest examples from the text file and
271
+ # filters out stopwords and pseudocode.
272
+ parser = DTParser(config=self.config.dt_config)
273
+
274
+ # This part of the code is unchanged
275
+ test = parser.get_doctest(text, globs, name, filename, 0)
276
+ if test.examples:
277
+ yield pydoctest.DoctestItem.from_parent(
278
+ self, name=test.name, runner=runner, dtest=test
279
+ )
280
+
281
+
282
+ def _get_runner(config, verbose, optionflags):
283
+ """
284
+ Override function to return an instance of PytestDTRunner.
285
+
286
+ This function creates and returns an instance of PytestDTRunner, a custom runner class
287
+ that extends the behavior of DebugDTRunner for running doctests in pytest.
288
+ """
289
+ class PytestDTRunner(DebugDTRunner):
290
+ def run(self, test, compileflags=None, out=None, clear_globs=False):
291
+ """
292
+ Run tests in context managers.
293
+
294
+ Restore the errstate/print state after each docstring.
295
+ Also, make MPL backend non-GUI and close the figures.
296
+
297
+ The order of context managers is actually relevant. Consider
298
+ user_context_mgr that turns warnings into errors.
299
+
300
+ Additionally, suppose that MPL deprecates something and plt.something
301
+ starts issuing warnings. Now all of those become errors
302
+ *unless* the `mpl()` context mgr has a chance to filter them out
303
+ *before* they become errors in `config.user_context_mgr()`.
304
+ """
305
+ dt_config = config.dt_config
306
+
307
+ with np_errstate():
308
+ with dt_config.user_context_mgr(test):
309
+ with matplotlib_make_nongui():
310
+ # XXX: local_resourses needed? they seem to be, w/o pytest
311
+ with temp_cwd(test, dt_config.local_resources):
312
+ super().run(test, compileflags=compileflags, out=out, clear_globs=clear_globs)
313
+
314
+ """
315
+ Almost verbatim copy of `_pytest.doctest.PytestDoctestRunner` except we utilize
316
+ DTConfig's `nameerror_after_exception` attribute in place of doctest's `continue_on_failure`.
317
+ """
318
+ def report_failure(self, out, test, example, got):
319
+ failure = doctest.DocTestFailure(test, example, got)
320
+ if config.dt_config.nameerror_after_exception:
321
+ out.append(failure)
322
+ else:
323
+ raise failure
324
+
325
+ def report_unexpected_exception(self, out, test, example, exc_info):
326
+ if isinstance(exc_info[1], outcomes.OutcomeException):
327
+ raise exc_info[1]
328
+ if isinstance(exc_info[1], bdb.BdbQuit):
329
+ outcomes.exit("Quitting debugger")
330
+ failure = doctest.UnexpectedException(test, example, exc_info)
331
+ if config.dt_config.nameerror_after_exception:
332
+ out.append(failure)
333
+ else:
334
+ raise failure
335
+
336
+ return PytestDTRunner(verbose=verbose, optionflags=optionflags, config=config.dt_config)
File without changes
@@ -0,0 +1,17 @@
1
+ __all__ = ['func9', 'func10']
2
+
3
+ def func9():
4
+ """
5
+ Wrong output.
6
+ >>> import numpy as np
7
+ >>> np.array([1, 2, 3])
8
+ array([2, 3, 4])
9
+ """
10
+
11
+
12
+ def func10():
13
+ """
14
+ NameError
15
+ >>> import numpy as np
16
+ >>> np.arraY([1, 2, 3])
17
+ """
@@ -0,0 +1,27 @@
1
+ __all__ = ['func_depr', 'func_name_error']
2
+
3
+ def func_depr():
4
+ """
5
+ A test case for the user context mgr to turn warnings to errors.
6
+
7
+ >>> import warnings; warnings.warn('Sample deprecation warning', DeprecationWarning)
8
+ """
9
+
10
+
11
+ def func_name_error():
12
+ """After an example fails, next examples may emit NameErrors. Suppress them.
13
+
14
+ Note that the suppresion is in effect for the duration of the DocTest, i.e.
15
+ for the whole docstring. Maybe this can be fixed at some point.
16
+
17
+ >>> def func():
18
+ ... raise ValueError("oops")
19
+ ... return 42
20
+ >>> res = func()
21
+ >>> res
22
+ >>> raise NameError('This is legitimate, but is suppressed')
23
+
24
+ Further name errors are also suppressed (which is a bug, too):
25
+ >>> raise NameError('Also legit')
26
+ """
27
+
@@ -0,0 +1,65 @@
1
+ """
2
+ A set of simple cases for DocTestFinder / DTFinder and its helpers.
3
+
4
+
5
+
6
+ 1. There is a doctest in the module docstring
7
+
8
+ >>> 1 + 2
9
+ 3
10
+ """
11
+
12
+ __all__ = ['func', 'Klass']
13
+
14
+
15
+ def func():
16
+ """Two doctests in a module-level function.
17
+
18
+ >>> 1 + 3
19
+ 4
20
+
21
+ >>> 5 + 6
22
+ 11
23
+ """
24
+ pass
25
+
26
+
27
+ class Klass:
28
+ """A class has doctests in a class docstring.
29
+
30
+ >>> 1 + 8
31
+ 9
32
+ """
33
+ def meth(self):
34
+ """And a method has its doctests.
35
+
36
+ >>> 2 + 11
37
+ 13
38
+ """
39
+ pass
40
+
41
+ def meth_2(self):
42
+ """
43
+ One other method.
44
+
45
+ >>> 111 + 1
46
+ 112
47
+ """
48
+
49
+
50
+ def private_func():
51
+ """A non-public function (not listed in __all__) also has examples.
52
+
53
+ >>> 9 + 11
54
+ 20
55
+ """
56
+
57
+
58
+ def _underscored_private_func():
59
+ """A private function (starts with an undescore) also has examples.
60
+
61
+ >>> 9 + 12
62
+ 21
63
+ """
64
+
65
+
File without changes
@@ -0,0 +1,37 @@
1
+ from ..conftest import dt_config
2
+
3
+ # Specify local files required by doctests
4
+ dt_config.local_resources = {
5
+ 'scipy_doctest.tests.local_file_cases.local_files': ['local_file.txt'],
6
+ 'scipy_doctest.local_file_cases.sio': ['octave_a.mat']
7
+ }
8
+
9
+
10
+ __all__ = ['local_files', 'sio']
11
+
12
+
13
+ def local_files():
14
+ """
15
+ A doctest that tries to read a local file
16
+
17
+ >>> with open('local_file.txt', 'r'):
18
+ ... pass
19
+ """
20
+
21
+
22
+ def sio():
23
+ """
24
+ The .mat file is from scipy/tutorial/io.rst; The test checks that a want/got
25
+ being a dict is handled correctly.
26
+
27
+ >>> import scipy.io as sio
28
+ >>> sio.loadmat('octave_a.mat')
29
+ {'__header__': b'MATLAB 5.0 MAT-file, written by Octave 3.2.3, 2010-05-30 02:13:40 UTC',
30
+ '__version__': '1.0',
31
+ '__globals__': [],
32
+ 'a': array([[[ 1., 4., 7., 10.],
33
+ [ 2., 5., 8., 11.],
34
+ [ 3., 6., 9., 12.]]])}
35
+ """
36
+
37
+
@@ -0,0 +1,192 @@
1
+ __all__ = [
2
+ 'func', 'func2', 'func3', 'func4', 'func5', 'func6', 'func8',
3
+ 'func7', 'manip_printoptions', 'array_abbreviation'
4
+ ]
5
+
6
+ import numpy as np
7
+ import pytest
8
+
9
+ def func():
10
+ """
11
+ >>> 2 / 3
12
+ 0.667
13
+ """
14
+ pass
15
+
16
+
17
+ def func2():
18
+ """
19
+ Check that `np.` is imported and the array repr is recognized. Also check
20
+ that whitespace is irrelevant for the checker.
21
+ >>> import numpy as np
22
+ >>> np.array([1, 2, 3.0])
23
+ array([1, 2, 3])
24
+
25
+ Check that the comparison is with atol and rtol: give less digits than
26
+ what numpy by default prints
27
+ >>> np.sin([1., 2, 3])
28
+ array([0.8414, 0.9092, 0.1411])
29
+
30
+ Also check that numpy repr for e.g. dtypes is recognized
31
+ >>> np.array([1, 2, 3], dtype=np.float32)
32
+ array([1., 2., 3.], dtype=float32)
33
+
34
+ """
35
+
36
+
37
+ def func3():
38
+ """
39
+ Check that printed arrays are checked with atol/rtol
40
+ >>> import numpy as np
41
+ >>> a = np.array([1, 2, 3, 4]) / 3
42
+ >>> print(a)
43
+ [0.33 0.66 1 1.33]
44
+ """
45
+
46
+
47
+ def func4():
48
+ """
49
+ Test `# may vary` markers : these should not break doctests (but the code
50
+ should still be valid, otherwise it's an error).
51
+ >>> import numpy as np
52
+ >>> np.random.randint(50)
53
+ 42 # may vary
54
+
55
+ >>> np.random.randint(50)
56
+ 42 # Random
57
+
58
+ >>> np.random.randint(50)
59
+ 42 # random
60
+ """
61
+
62
+
63
+ def func5():
64
+ """
65
+ Object addresses are ignored:
66
+ >>> import numpy as np
67
+ >>> np.array([1, 2, 3]).data
68
+ <memory at 0x7f119b952400>
69
+ """
70
+
71
+
72
+ def func6():
73
+ """
74
+ Masked arrays
75
+
76
+ >>> import numpy.ma as ma
77
+ >>> y = ma.array([1, 2, 3], mask = [0, 1, 0])
78
+ >>> y
79
+ masked_array(data=[1, --, 3],
80
+ mask=[False, True, False],
81
+ fill_value=999999)
82
+
83
+ """
84
+
85
+
86
+ def func8():
87
+ """
88
+ Namedtuples (they are *not* in the namespace)
89
+
90
+ >>> from scipy.stats import levene
91
+ >>> a = [8.88, 9.12, 9.04, 8.98, 9.00, 9.08, 9.01, 8.85, 9.06, 8.99]
92
+ >>> b = [8.88, 8.95, 9.29, 9.44, 9.15, 9.58, 8.36, 9.18, 8.67, 9.05]
93
+ >>> c = [8.95, 9.12, 8.95, 8.85, 9.03, 8.84, 9.07, 8.98, 8.86, 8.98]
94
+
95
+ # namedtuples are recognized...
96
+ >>> levene(a, b, c)
97
+ LeveneResult(statistic=7.58495, pvalue=0.00243)
98
+
99
+ # can be reformatted
100
+ >>> levene(a, b, c)
101
+ LeveneResult(statistic=7.58495,
102
+ pvalue=0.00243)
103
+
104
+ # ... or can be given just as tuples
105
+ >>> levene(a, b, c)
106
+ (7.58495, 0.00243)
107
+
108
+ """
109
+
110
+
111
+ def func7():
112
+ """
113
+ Multiline namedtuples + nested tuples, see scipy/gh-16082
114
+
115
+ >>> from scipy import stats
116
+ >>> import numpy as np
117
+ >>> a = np.arange(10)
118
+ >>> stats.describe(a)
119
+ DescribeResult(nobs=10, minmax=(0, 9), mean=4.5,
120
+ variance=9.16666666666, skewness=0.0,
121
+ kurtosis=-1.224242424)
122
+
123
+ A single-line form of a namedtuple
124
+ >>> stats.describe(a)
125
+ DescribeResult(nobs=10, minmax=(0, 9), mean=4.5, variance=9.16666, skewness=0.0, kurtosis=-1.2242424)
126
+
127
+ """
128
+
129
+
130
+ def manip_printoptions():
131
+ """Manipulate np.printoptions.
132
+ >>> import numpy as np
133
+ >>> np.set_printoptions(linewidth=146)
134
+ """
135
+
136
+
137
+ def array_abbreviation():
138
+ """
139
+ Numpy abbreviates arrays, check that it works.
140
+
141
+ NB: the implementation might need to change when
142
+ numpy finally disallows default-creating ragged arrays.
143
+ Currently, `...` gets interpreted as an Ellipsis,
144
+ thus the `a_want/a_got` variables in DTChecker are in fact
145
+ object arrays.
146
+ >>> import numpy as np
147
+ >>> np.arange(10000)
148
+ array([0, 1, 2, ..., 9997, 9998, 9999])
149
+
150
+ >>> np.diag(np.arange(33)) / 30
151
+ array([[0., 0., 0., ..., 0., 0.,0.],
152
+ [0., 0.03333333, 0., ..., 0., 0., 0.],
153
+ [0., 0., 0.06666667, ..., 0., 0., 0.],
154
+ ...,
155
+ [0., 0., 0., ..., 1., 0., 0.],
156
+ [0., 0., 0., ..., 0., 1.03333333, 0.],
157
+ [0., 0., 0., ..., 0., 0., 1.06666667]])
158
+
159
+
160
+ >>> np.diag(np.arange(1, 1001, dtype=float))
161
+ array([[1, 0, 0, ..., 0, 0, 0],
162
+ [0, 2, 0, ..., 0, 0, 0],
163
+ [0, 0, 3, ..., 0, 0, 0],
164
+ ...,
165
+ [0, 0, 0, ..., 998, 0, 0],
166
+ [0, 0, 0, ..., 0, 999, 0],
167
+ [0, 0, 0, ..., 0, 0, 1000]])
168
+ """
169
+
170
+ def nan_equal():
171
+ """
172
+ Test that nans are treated as equal.
173
+
174
+ >>> import numpy as np
175
+ >>> np.nan
176
+ np.float64(nan)
177
+ """
178
+
179
+
180
+ def test_cmplx_nan():
181
+ """
182
+ Complex nans
183
+ >>> import numpy as np
184
+ >>> np.nan - 1j*np.nan
185
+ nan + nanj
186
+
187
+ >>> np.nan + 1j*np.nan
188
+ np.complex128(nan+nanj)
189
+
190
+ >>> 1j*np.complex128(np.nan)
191
+ np.complex128(nan+nanj)
192
+ """
Binary file