scipy-doctest 1.7.1__tar.gz → 2.0.0__tar.gz
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-1.7.1 → scipy_doctest-2.0.0}/PKG-INFO +62 -26
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/README.md +60 -24
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/pyproject.toml +1 -1
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/__init__.py +1 -1
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/impl.py +26 -1
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/plugin.py +46 -7
- scipy_doctest-2.0.0/scipy_doctest/tests/failure_cases.py +118 -0
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/tests/module_cases.py +37 -5
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/tests/test_testmod.py +24 -0
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/util.py +18 -0
- scipy_doctest-1.7.1/scipy_doctest/tests/failure_cases.py +0 -55
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/LICENSE +0 -0
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/__main__.py +0 -0
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/conftest.py +0 -0
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/frontend.py +0 -0
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/tests/__init__.py +0 -0
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/tests/failure_cases_2.py +0 -0
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/tests/finder_cases.py +0 -0
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/tests/finder_cases_2.py +0 -0
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/tests/local_file.txt +0 -0
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/tests/local_file_cases.py +0 -0
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/tests/octave_a.mat +0 -0
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/tests/scipy_ndimage_tutorial_clone.rst +0 -0
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/tests/stopwords_cases.py +0 -0
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/tests/test_finder.py +0 -0
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/tests/test_parser.py +0 -0
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/tests/test_pytest_configuration.py +0 -0
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/tests/test_runner.py +0 -0
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/tests/test_skipmarkers.py +0 -0
- {scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/tests/test_testfile.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scipy_doctest
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Configurable, whitespace-insensitive, floating-point-aware doctest helpers.
|
|
5
5
|
Maintainer-email: SciPy developers <scipy-dev@python.org>
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -17,7 +17,7 @@ Requires-Dist: furo==2024.8.6 ; extra == "doc"
|
|
|
17
17
|
Requires-Dist: myst-parser==4.0.0 ; extra == "doc"
|
|
18
18
|
Requires-Dist: sphinx==8.1.3 ; extra == "doc"
|
|
19
19
|
Requires-Dist: sphinx-copybutton==0.5.2 ; extra == "doc"
|
|
20
|
-
Requires-Dist: scipy
|
|
20
|
+
Requires-Dist: scipy ; extra == "test"
|
|
21
21
|
Requires-Dist: matplotlib ; extra == "test"
|
|
22
22
|
Project-URL: Home, https://github.com/scipy/scipy_doctest
|
|
23
23
|
Provides-Extra: doc
|
|
@@ -176,6 +176,21 @@ $ pytest --pyargs <your-package> --doctest-modules --doctest-collect=api
|
|
|
176
176
|
See [More fine-grained control](#more-fine-grained-control) section
|
|
177
177
|
for details on how to customize the behavior.
|
|
178
178
|
|
|
179
|
+
**NOTE** In versions 1.x, `pytest --doctest-modules` was only collecting doctests, and
|
|
180
|
+
skipped 'regular' unit tests. This differed from the vanilla `pytest` behavior, which
|
|
181
|
+
collects both doctests and unit tests.
|
|
182
|
+
|
|
183
|
+
The behavior was changed in version 2.0: from `scipy-doctest==2.0` the default is
|
|
184
|
+
changed to align with the vanilla `pytest`.
|
|
185
|
+
|
|
186
|
+
To retain the previous behavior and skip 'regular' unit tests, use the
|
|
187
|
+
`--doctest-only-doctests` CLI option:
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
$ pytest --doctest-modules --doctest-only-doctests=true
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
|
|
179
194
|
### Basic usage
|
|
180
195
|
|
|
181
196
|
The use of `pytest` is optional, and you can use the `doctest` layer API.
|
|
@@ -316,18 +331,18 @@ NumPy wraps `scipy-doctest` with the `spin` command
|
|
|
316
331
|
$ spin check-docs
|
|
317
332
|
```
|
|
318
333
|
|
|
319
|
-
SciPy
|
|
334
|
+
In SciPy, the name of the `spin` command is `smoke-docs`::
|
|
320
335
|
|
|
321
336
|
```
|
|
322
|
-
$
|
|
323
|
-
$
|
|
337
|
+
$ spin smoke-docs # check docstrings
|
|
338
|
+
$ spin smoke-tutorials # ReST user guide tutorials
|
|
324
339
|
```
|
|
325
340
|
|
|
326
341
|
## Rough edges and sharp bits
|
|
327
342
|
|
|
328
343
|
Here is a (non-exhaustive) list of possible gotchas:
|
|
329
344
|
|
|
330
|
-
|
|
345
|
+
#### _In-place development builds_.
|
|
331
346
|
|
|
332
347
|
Some tools (looking at you `meson-python`) simulate in-place builds with a
|
|
333
348
|
`build-install` directory. If this directory is located under the project root,
|
|
@@ -351,10 +366,10 @@ If push really comes to shove, you may try using the magic env variable:
|
|
|
351
366
|
however the need usually indicates an issue with the package itself.
|
|
352
367
|
(see [gh-107](https://github.com/scipy/scipy_doctest/pull/107) for an example).
|
|
353
368
|
|
|
354
|
-
|
|
369
|
+
#### _Optional dependencies are not that optional_
|
|
355
370
|
|
|
356
371
|
If your package contains optional dependencies, doctests do not know about them
|
|
357
|
-
being optional. So you either guard the imports in doctests (yikes!), or
|
|
372
|
+
being optional. So you either guard the imports in doctests themselves (yikes!), or
|
|
358
373
|
the collections fails if dependencies are not available.
|
|
359
374
|
|
|
360
375
|
The solution is to explicitly `--ignore` the paths to modules with optionals.
|
|
@@ -370,7 +385,7 @@ Note that installed packages are no different:
|
|
|
370
385
|
$ pytest --pyargs scipy --doctest-modules --ignore=/path/to/installed/scipy/_lib
|
|
371
386
|
```
|
|
372
387
|
|
|
373
|
-
|
|
388
|
+
#### _Doctest collection strategies_
|
|
374
389
|
|
|
375
390
|
The default collection strategy follows `doctest` module and `pytest`. This leads
|
|
376
391
|
to duplicates if your package has the split between public and \_private modules,
|
|
@@ -380,19 +395,19 @@ objects will be collected.
|
|
|
380
395
|
|
|
381
396
|
The decision on what is public is as follows: an object is public iff
|
|
382
397
|
|
|
383
|
-
- it is included into the `__all__` list of a public module;
|
|
384
|
-
- the name of the object does not have a leading underscore;
|
|
385
|
-
- the name of the module from which the object is collected does not have
|
|
398
|
+
- it is included into the `__all__` list of a public module;
|
|
399
|
+
- the name of the object does not have a leading underscore;
|
|
400
|
+
- the name of the module from which the object is collected does not have
|
|
386
401
|
a leading underscore.
|
|
387
402
|
|
|
388
403
|
Consider an example: `scipy.linalg.det` is defined in `scipy/linalg/_basic.py`,
|
|
389
404
|
so it is collected twice, from `_basic.py` and from `__init__.py`. The rule above
|
|
390
405
|
leads to
|
|
391
406
|
|
|
392
|
-
- `scipy.linalg._basic.det`, collected from `scipy/linalg/_basic.py`, is private.
|
|
393
|
-
- `scipy.linalg.det`, collected from `scipy/linalg/__init__.py`, is public.
|
|
407
|
+
- `scipy.linalg._basic.det`, collected from `scipy/linalg/_basic.py`, is private.
|
|
408
|
+
- `scipy.linalg.det`, collected from `scipy/linalg/__init__.py`, is public.
|
|
394
409
|
|
|
395
|
-
|
|
410
|
+
#### _`pytest`'s assertion rewriting_
|
|
396
411
|
|
|
397
412
|
In some rare cases you may need to either explicitly register the `scipy_doctest`
|
|
398
413
|
package with the `pytest` assertion rewriting machinery, or ask it to avoid rewriting
|
|
@@ -404,6 +419,21 @@ In general, rewriting assertions is not very useful for doctests, as the
|
|
|
404
419
|
output on error is fixed by the doctest machinery anyway. Therefore, we believe
|
|
405
420
|
adding `--assert=plain` is reasonable.
|
|
406
421
|
|
|
422
|
+
#### _Mixing strings and numbers_
|
|
423
|
+
|
|
424
|
+
Generally, we aim to handle mixtures of strings and numeric data. Deeply nested data
|
|
425
|
+
structures, however, may cause the checker to fall back to the vanilla `doctest` literal
|
|
426
|
+
checking. For instance, `["value", 1/3]` will use the floating-point aware checker, and
|
|
427
|
+
so will `{"value": 1/3, "other value": 2/3}` or `[(1, 2), (3, 4)]`; Meanwhile, nested
|
|
428
|
+
dictionaries, `{"a": dict(value=1/3)}`, or lists of tuples with mixed entries,
|
|
429
|
+
`[("a", 1/3), ("b", 2/3)]`, will currently fall back to vanilla `doctest` literal
|
|
430
|
+
comparisons.
|
|
431
|
+
|
|
432
|
+
We stress that no matter how tricky or deeply nested the output is, the worst case
|
|
433
|
+
scenario is that the floating-point aware checker is not used. If you have a case where
|
|
434
|
+
`doctest` works correctly and `scipy_doctest` does not, please report it as a bug.
|
|
435
|
+
|
|
436
|
+
|
|
407
437
|
## Prior art and related work
|
|
408
438
|
|
|
409
439
|
- `pytest` provides some limited floating-point aware `NumericLiteralChecker`.
|
|
@@ -411,24 +441,30 @@ adding `--assert=plain` is reasonable.
|
|
|
411
441
|
- `pytest-doctestplus` plugin from the `AstroPy` project has similar goals.
|
|
412
442
|
The package is well established and widely used. From a user perspective, main
|
|
413
443
|
differences are: (i) `pytest-doctestplus` is more sensitive to formatting,
|
|
414
|
-
including whitespace
|
|
415
|
-
may start failing; (ii) there is still a need for `# doctest: +FLOAT_CMP`
|
|
444
|
+
including whitespace; (ii) there is still a need for `# doctest: +FLOAT_CMP`
|
|
416
445
|
directives.
|
|
417
446
|
|
|
418
|
-
This project takes a different approach:
|
|
419
|
-
|
|
420
|
-
|
|
447
|
+
This project takes a slightly different approach: we strive to make numeric comparisons
|
|
448
|
+
whitespace insensitive and automatic, without a need for explicit markup. For rare cases
|
|
449
|
+
which require additional configuration, we either keep it in the tool (thus out of
|
|
450
|
+
reader-visible docstrings), or provide human-readable markers (hence `# may vary`
|
|
451
|
+
not `# doctest +SKIP`).
|
|
452
|
+
Furthermore, in addition to plugging into `pytest`, we provide an API layer which closely
|
|
453
|
+
follows the `doctest` API. Essentially all aspects of doctesting are user-configurable.
|
|
454
|
+
|
|
455
|
+
- `xdoctest` package relies on a deeper rewrite of the standard-library `doctest`
|
|
456
|
+
functionality, and uses an AST-based analysis to parse code examples out of docstrings.
|
|
421
457
|
|
|
422
458
|
- `NumPy` and `SciPy` were using modified doctesting in their `refguide-check` utilities.
|
|
423
|
-
These utilities
|
|
459
|
+
These utilities were tightly coupled to their libraries, and have been reported
|
|
424
460
|
to be not easy to reason about, work with, and extend to other projects.
|
|
425
461
|
|
|
426
|
-
This project is mainly the core functionality of the modified
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
462
|
+
This project is mainly the core functionality of the modified `refguide-check` doctesting,
|
|
463
|
+
extracted to a separate package. We believe having it separate simplifies both
|
|
464
|
+
addressing the needs of these two packages, and adoption by other projects.
|
|
465
|
+
|
|
430
466
|
|
|
431
|
-
|
|
467
|
+
## Bug reports, feature requests and contributions
|
|
432
468
|
|
|
433
469
|
This package is work in progress. Contributions are most welcome!
|
|
434
470
|
Please don't hesitate to open an issue in the tracker or send a pull request.
|
|
@@ -151,6 +151,21 @@ $ pytest --pyargs <your-package> --doctest-modules --doctest-collect=api
|
|
|
151
151
|
See [More fine-grained control](#more-fine-grained-control) section
|
|
152
152
|
for details on how to customize the behavior.
|
|
153
153
|
|
|
154
|
+
**NOTE** In versions 1.x, `pytest --doctest-modules` was only collecting doctests, and
|
|
155
|
+
skipped 'regular' unit tests. This differed from the vanilla `pytest` behavior, which
|
|
156
|
+
collects both doctests and unit tests.
|
|
157
|
+
|
|
158
|
+
The behavior was changed in version 2.0: from `scipy-doctest==2.0` the default is
|
|
159
|
+
changed to align with the vanilla `pytest`.
|
|
160
|
+
|
|
161
|
+
To retain the previous behavior and skip 'regular' unit tests, use the
|
|
162
|
+
`--doctest-only-doctests` CLI option:
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
$ pytest --doctest-modules --doctest-only-doctests=true
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
|
|
154
169
|
### Basic usage
|
|
155
170
|
|
|
156
171
|
The use of `pytest` is optional, and you can use the `doctest` layer API.
|
|
@@ -291,18 +306,18 @@ NumPy wraps `scipy-doctest` with the `spin` command
|
|
|
291
306
|
$ spin check-docs
|
|
292
307
|
```
|
|
293
308
|
|
|
294
|
-
SciPy
|
|
309
|
+
In SciPy, the name of the `spin` command is `smoke-docs`::
|
|
295
310
|
|
|
296
311
|
```
|
|
297
|
-
$
|
|
298
|
-
$
|
|
312
|
+
$ spin smoke-docs # check docstrings
|
|
313
|
+
$ spin smoke-tutorials # ReST user guide tutorials
|
|
299
314
|
```
|
|
300
315
|
|
|
301
316
|
## Rough edges and sharp bits
|
|
302
317
|
|
|
303
318
|
Here is a (non-exhaustive) list of possible gotchas:
|
|
304
319
|
|
|
305
|
-
|
|
320
|
+
#### _In-place development builds_.
|
|
306
321
|
|
|
307
322
|
Some tools (looking at you `meson-python`) simulate in-place builds with a
|
|
308
323
|
`build-install` directory. If this directory is located under the project root,
|
|
@@ -326,10 +341,10 @@ If push really comes to shove, you may try using the magic env variable:
|
|
|
326
341
|
however the need usually indicates an issue with the package itself.
|
|
327
342
|
(see [gh-107](https://github.com/scipy/scipy_doctest/pull/107) for an example).
|
|
328
343
|
|
|
329
|
-
|
|
344
|
+
#### _Optional dependencies are not that optional_
|
|
330
345
|
|
|
331
346
|
If your package contains optional dependencies, doctests do not know about them
|
|
332
|
-
being optional. So you either guard the imports in doctests (yikes!), or
|
|
347
|
+
being optional. So you either guard the imports in doctests themselves (yikes!), or
|
|
333
348
|
the collections fails if dependencies are not available.
|
|
334
349
|
|
|
335
350
|
The solution is to explicitly `--ignore` the paths to modules with optionals.
|
|
@@ -345,7 +360,7 @@ Note that installed packages are no different:
|
|
|
345
360
|
$ pytest --pyargs scipy --doctest-modules --ignore=/path/to/installed/scipy/_lib
|
|
346
361
|
```
|
|
347
362
|
|
|
348
|
-
|
|
363
|
+
#### _Doctest collection strategies_
|
|
349
364
|
|
|
350
365
|
The default collection strategy follows `doctest` module and `pytest`. This leads
|
|
351
366
|
to duplicates if your package has the split between public and \_private modules,
|
|
@@ -355,19 +370,19 @@ objects will be collected.
|
|
|
355
370
|
|
|
356
371
|
The decision on what is public is as follows: an object is public iff
|
|
357
372
|
|
|
358
|
-
- it is included into the `__all__` list of a public module;
|
|
359
|
-
- the name of the object does not have a leading underscore;
|
|
360
|
-
- the name of the module from which the object is collected does not have
|
|
373
|
+
- it is included into the `__all__` list of a public module;
|
|
374
|
+
- the name of the object does not have a leading underscore;
|
|
375
|
+
- the name of the module from which the object is collected does not have
|
|
361
376
|
a leading underscore.
|
|
362
377
|
|
|
363
378
|
Consider an example: `scipy.linalg.det` is defined in `scipy/linalg/_basic.py`,
|
|
364
379
|
so it is collected twice, from `_basic.py` and from `__init__.py`. The rule above
|
|
365
380
|
leads to
|
|
366
381
|
|
|
367
|
-
- `scipy.linalg._basic.det`, collected from `scipy/linalg/_basic.py`, is private.
|
|
368
|
-
- `scipy.linalg.det`, collected from `scipy/linalg/__init__.py`, is public.
|
|
382
|
+
- `scipy.linalg._basic.det`, collected from `scipy/linalg/_basic.py`, is private.
|
|
383
|
+
- `scipy.linalg.det`, collected from `scipy/linalg/__init__.py`, is public.
|
|
369
384
|
|
|
370
|
-
|
|
385
|
+
#### _`pytest`'s assertion rewriting_
|
|
371
386
|
|
|
372
387
|
In some rare cases you may need to either explicitly register the `scipy_doctest`
|
|
373
388
|
package with the `pytest` assertion rewriting machinery, or ask it to avoid rewriting
|
|
@@ -379,6 +394,21 @@ In general, rewriting assertions is not very useful for doctests, as the
|
|
|
379
394
|
output on error is fixed by the doctest machinery anyway. Therefore, we believe
|
|
380
395
|
adding `--assert=plain` is reasonable.
|
|
381
396
|
|
|
397
|
+
#### _Mixing strings and numbers_
|
|
398
|
+
|
|
399
|
+
Generally, we aim to handle mixtures of strings and numeric data. Deeply nested data
|
|
400
|
+
structures, however, may cause the checker to fall back to the vanilla `doctest` literal
|
|
401
|
+
checking. For instance, `["value", 1/3]` will use the floating-point aware checker, and
|
|
402
|
+
so will `{"value": 1/3, "other value": 2/3}` or `[(1, 2), (3, 4)]`; Meanwhile, nested
|
|
403
|
+
dictionaries, `{"a": dict(value=1/3)}`, or lists of tuples with mixed entries,
|
|
404
|
+
`[("a", 1/3), ("b", 2/3)]`, will currently fall back to vanilla `doctest` literal
|
|
405
|
+
comparisons.
|
|
406
|
+
|
|
407
|
+
We stress that no matter how tricky or deeply nested the output is, the worst case
|
|
408
|
+
scenario is that the floating-point aware checker is not used. If you have a case where
|
|
409
|
+
`doctest` works correctly and `scipy_doctest` does not, please report it as a bug.
|
|
410
|
+
|
|
411
|
+
|
|
382
412
|
## Prior art and related work
|
|
383
413
|
|
|
384
414
|
- `pytest` provides some limited floating-point aware `NumericLiteralChecker`.
|
|
@@ -386,24 +416,30 @@ adding `--assert=plain` is reasonable.
|
|
|
386
416
|
- `pytest-doctestplus` plugin from the `AstroPy` project has similar goals.
|
|
387
417
|
The package is well established and widely used. From a user perspective, main
|
|
388
418
|
differences are: (i) `pytest-doctestplus` is more sensitive to formatting,
|
|
389
|
-
including whitespace
|
|
390
|
-
may start failing; (ii) there is still a need for `# doctest: +FLOAT_CMP`
|
|
419
|
+
including whitespace; (ii) there is still a need for `# doctest: +FLOAT_CMP`
|
|
391
420
|
directives.
|
|
392
421
|
|
|
393
|
-
This project takes a different approach:
|
|
394
|
-
|
|
395
|
-
|
|
422
|
+
This project takes a slightly different approach: we strive to make numeric comparisons
|
|
423
|
+
whitespace insensitive and automatic, without a need for explicit markup. For rare cases
|
|
424
|
+
which require additional configuration, we either keep it in the tool (thus out of
|
|
425
|
+
reader-visible docstrings), or provide human-readable markers (hence `# may vary`
|
|
426
|
+
not `# doctest +SKIP`).
|
|
427
|
+
Furthermore, in addition to plugging into `pytest`, we provide an API layer which closely
|
|
428
|
+
follows the `doctest` API. Essentially all aspects of doctesting are user-configurable.
|
|
429
|
+
|
|
430
|
+
- `xdoctest` package relies on a deeper rewrite of the standard-library `doctest`
|
|
431
|
+
functionality, and uses an AST-based analysis to parse code examples out of docstrings.
|
|
396
432
|
|
|
397
433
|
- `NumPy` and `SciPy` were using modified doctesting in their `refguide-check` utilities.
|
|
398
|
-
These utilities
|
|
434
|
+
These utilities were tightly coupled to their libraries, and have been reported
|
|
399
435
|
to be not easy to reason about, work with, and extend to other projects.
|
|
400
436
|
|
|
401
|
-
This project is mainly the core functionality of the modified
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
437
|
+
This project is mainly the core functionality of the modified `refguide-check` doctesting,
|
|
438
|
+
extracted to a separate package. We believe having it separate simplifies both
|
|
439
|
+
addressing the needs of these two packages, and adoption by other projects.
|
|
440
|
+
|
|
405
441
|
|
|
406
|
-
|
|
442
|
+
## Bug reports, feature requests and contributions
|
|
407
443
|
|
|
408
444
|
This package is work in progress. Contributions are most welcome!
|
|
409
445
|
Please don't hesitate to open an issue in the tracker or send a pull request.
|
|
@@ -2,6 +2,7 @@ import re
|
|
|
2
2
|
import warnings
|
|
3
3
|
import inspect
|
|
4
4
|
import doctest
|
|
5
|
+
import operator
|
|
5
6
|
from doctest import NORMALIZE_WHITESPACE, ELLIPSIS, IGNORE_EXCEPTION_DETAIL
|
|
6
7
|
from itertools import zip_longest
|
|
7
8
|
from sys import version_info
|
|
@@ -78,7 +79,7 @@ class DTConfig:
|
|
|
78
79
|
Default is False.
|
|
79
80
|
pytest_extra_ignore : list
|
|
80
81
|
A list of names/modules to ignore when run under pytest plugin. This is
|
|
81
|
-
equivalent to using
|
|
82
|
+
equivalent to using ``--ignore=...`` cmdline switch.
|
|
82
83
|
pytest_extra_skip : dict
|
|
83
84
|
Names/modules to skip when run under pytest plugin. This is
|
|
84
85
|
equivalent to decorating the doctest with `@pytest.mark.skip` or adding
|
|
@@ -91,6 +92,12 @@ class DTConfig:
|
|
|
91
92
|
adding `# may vary` to the outputs of all examples.
|
|
92
93
|
Each key is a doctest name to skip, and the corresponding value is
|
|
93
94
|
a string. If not empty, the string value is used as the skip reason.
|
|
95
|
+
pytest_extra_requires : dict
|
|
96
|
+
Paths or functions to conditionally ignore unless requirements are met.
|
|
97
|
+
The format is ``{path/or/glob/pattern: requirement(s), full.func.name: requirement(s)}``,
|
|
98
|
+
where the values are PEP 508 dependency specifiers. If a requirement is not met,
|
|
99
|
+
the behavior is equivalent to using the ``--ignore=...`` command line switch for
|
|
100
|
+
paths, and to using a `pytest_extra_skip` for function names.
|
|
94
101
|
CheckerKlass : object, optional
|
|
95
102
|
The class for the Checker object. Must mimic the ``DTChecker`` API:
|
|
96
103
|
subclass the `doctest.OutputChecker` and make the constructor signature
|
|
@@ -124,6 +131,7 @@ class DTConfig:
|
|
|
124
131
|
pytest_extra_ignore=None,
|
|
125
132
|
pytest_extra_skip=None,
|
|
126
133
|
pytest_extra_xfail=None,
|
|
134
|
+
pytest_extra_requires=None,
|
|
127
135
|
):
|
|
128
136
|
### DTChecker configuration ###
|
|
129
137
|
self.CheckerKlass = CheckerKlass or DTChecker
|
|
@@ -216,6 +224,7 @@ class DTConfig:
|
|
|
216
224
|
self.pytest_extra_ignore = pytest_extra_ignore or []
|
|
217
225
|
self.pytest_extra_skip = pytest_extra_skip or {}
|
|
218
226
|
self.pytest_extra_xfail = pytest_extra_xfail or {}
|
|
227
|
+
self.pytest_extra_requires = pytest_extra_requires or {}
|
|
219
228
|
|
|
220
229
|
|
|
221
230
|
def try_convert_namedtuple(got):
|
|
@@ -413,6 +422,22 @@ class DTChecker(doctest.OutputChecker):
|
|
|
413
422
|
if is_list_or_tuple and type(a_want) is not type(a_got):
|
|
414
423
|
return False
|
|
415
424
|
|
|
425
|
+
# dicts and other mappings need special treatment
|
|
426
|
+
want_is_dict = hasattr(a_want, 'items')
|
|
427
|
+
got_is_dict = hasattr(a_got, 'items')
|
|
428
|
+
if operator.xor(want_is_dict, got_is_dict):
|
|
429
|
+
# either both are dicts or both are not
|
|
430
|
+
return False
|
|
431
|
+
|
|
432
|
+
if want_is_dict:
|
|
433
|
+
# split each dict into a pair of lists of keys and values, and retry
|
|
434
|
+
want_keys, want_values = f"{list(a_want.keys())}", f"{list(a_want.values())}"
|
|
435
|
+
got_keys, got_values = f"{list(a_got.keys())}", f"{list(a_got.values())}"
|
|
436
|
+
|
|
437
|
+
keys_match = self.check_output(want_keys, got_keys, optionflags)
|
|
438
|
+
values_match = self.check_output(want_values, got_values, optionflags)
|
|
439
|
+
return keys_match and values_match
|
|
440
|
+
|
|
416
441
|
# ... and defer to numpy
|
|
417
442
|
strict = self.config.strict_check
|
|
418
443
|
try:
|
|
@@ -13,7 +13,7 @@ from _pytest.pathlib import import_path
|
|
|
13
13
|
|
|
14
14
|
from .impl import DTParser, DebugDTRunner
|
|
15
15
|
from .conftest import dt_config
|
|
16
|
-
from .util import np_errstate, matplotlib_make_nongui, temp_cwd
|
|
16
|
+
from .util import np_errstate, matplotlib_make_nongui, temp_cwd, is_req_satisfied
|
|
17
17
|
from .frontend import find_doctests
|
|
18
18
|
|
|
19
19
|
|
|
@@ -29,6 +29,28 @@ def pytest_addoption(parser):
|
|
|
29
29
|
dest="collection_strategy"
|
|
30
30
|
)
|
|
31
31
|
|
|
32
|
+
# We cannot add `--doctest-only` option because of
|
|
33
|
+
# https://github.com/pytest-dev/pytest/discussions/13435
|
|
34
|
+
#
|
|
35
|
+
# Therefore, we add a new option, --doctest-only-doctests,
|
|
36
|
+
# which was `true` by default in versions 1.x.
|
|
37
|
+
#
|
|
38
|
+
# In v2.0, the default is `false`, so that
|
|
39
|
+
#
|
|
40
|
+
# $ pytest --doctest-modules
|
|
41
|
+
#
|
|
42
|
+
# runs both doctests and unit tests, and the way to use the previous behavior
|
|
43
|
+
# (only run doctests, skip unit tests) is
|
|
44
|
+
#
|
|
45
|
+
# $ pytest --doctest-modules --doctest-only-doctests=true
|
|
46
|
+
#
|
|
47
|
+
group.addoption("--doctest-only-doctests",
|
|
48
|
+
action="store",
|
|
49
|
+
default="false",
|
|
50
|
+
help="Whether to only collect doctests, or also collect unit tests, too.",
|
|
51
|
+
choices=("true", "false"),
|
|
52
|
+
dest="doctest_only_doctests"
|
|
53
|
+
)
|
|
32
54
|
|
|
33
55
|
def pytest_configure(config):
|
|
34
56
|
"""
|
|
@@ -49,15 +71,27 @@ def pytest_ignore_collect(collection_path, config):
|
|
|
49
71
|
This function is used to exclude the 'tests' directory and test modules when
|
|
50
72
|
the `--doctest-modules` option is used.
|
|
51
73
|
"""
|
|
52
|
-
|
|
74
|
+
# XXX: From v2.0, --doctest-modules means "run both doctests and unit tests",
|
|
75
|
+
# (consistent with vanilla pytest), and the way to retain the 1.x behavior
|
|
76
|
+
# is to add --doctest-only-doctests=true to the CLI command
|
|
77
|
+
if (
|
|
78
|
+
config.getoption("--doctest-modules") and
|
|
79
|
+
config.getoption("--doctest-only-doctests") == 'true'
|
|
80
|
+
):
|
|
53
81
|
path_str = str(collection_path)
|
|
54
82
|
if "tests" in path_str or "test_" in path_str:
|
|
55
83
|
return True
|
|
56
84
|
|
|
85
|
+
fnmatch_ex = _pytest.pathlib.fnmatch_ex
|
|
86
|
+
|
|
57
87
|
for entry in config.dt_config.pytest_extra_ignore:
|
|
58
|
-
if entry
|
|
88
|
+
if fnmatch_ex(entry, collection_path):
|
|
59
89
|
return True
|
|
60
90
|
|
|
91
|
+
for entry, reqs in config.dt_config.pytest_extra_requires.items():
|
|
92
|
+
if fnmatch_ex(entry, collection_path):
|
|
93
|
+
return not is_req_satisfied(reqs)
|
|
94
|
+
|
|
61
95
|
|
|
62
96
|
def is_private(item):
|
|
63
97
|
"""Decide if an DocTestItem `item` is private.
|
|
@@ -82,21 +116,26 @@ def _maybe_add_markers(item, config):
|
|
|
82
116
|
dt_config = config.dt_config
|
|
83
117
|
|
|
84
118
|
extra_skip = dt_config.pytest_extra_skip
|
|
85
|
-
skip_it
|
|
86
|
-
if skip_it:
|
|
119
|
+
if skip_it := item.name in extra_skip:
|
|
87
120
|
reason = extra_skip[item.name] or ''
|
|
88
121
|
item.add_marker(
|
|
89
122
|
pytest.mark.skip(reason=reason)
|
|
90
123
|
)
|
|
91
124
|
|
|
92
125
|
extra_xfail = dt_config.pytest_extra_xfail
|
|
93
|
-
fail_it
|
|
94
|
-
if fail_it:
|
|
126
|
+
if fail_it := item.name in extra_xfail:
|
|
95
127
|
reason = extra_xfail[item.name] or ''
|
|
96
128
|
item.add_marker(
|
|
97
129
|
pytest.mark.xfail(reason=reason)
|
|
98
130
|
)
|
|
99
131
|
|
|
132
|
+
extra_requires = dt_config.pytest_extra_requires
|
|
133
|
+
if req_str := extra_requires.get(item.name, None):
|
|
134
|
+
if not is_req_satisfied(req_str):
|
|
135
|
+
item.add_marker(
|
|
136
|
+
pytest.mark.skip(reason=f"requires {req_str}")
|
|
137
|
+
)
|
|
138
|
+
|
|
100
139
|
|
|
101
140
|
def pytest_collection_modifyitems(config, items):
|
|
102
141
|
"""
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
__all__ = ['func9', 'func10', 'iterable_length_1', 'iterable_length_2',
|
|
2
|
+
'tuple_and_list_1', 'tuple_and_list_2']
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def func9():
|
|
6
|
+
"""
|
|
7
|
+
Wrong output.
|
|
8
|
+
>>> import numpy as np
|
|
9
|
+
>>> np.array([1, 2, 3])
|
|
10
|
+
array([2, 3, 4])
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def func10():
|
|
15
|
+
"""
|
|
16
|
+
NameError
|
|
17
|
+
>>> import numpy as np
|
|
18
|
+
>>> np.arraY([1, 2, 3])
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def iterable_length_1():
|
|
23
|
+
"""
|
|
24
|
+
>>> [1, 2, 3]
|
|
25
|
+
[1, 2, 3, 4]
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def iterable_length_2():
|
|
30
|
+
"""
|
|
31
|
+
>>> [1, 2, 3]
|
|
32
|
+
[1, 2]
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def tuple_and_list_1():
|
|
37
|
+
"""
|
|
38
|
+
>>> [0, 1, 2]
|
|
39
|
+
(0, 1, 2)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def tuple_and_list_2():
|
|
44
|
+
"""
|
|
45
|
+
>>> (0, 1, 2)
|
|
46
|
+
[0, 1, 2]
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def dtype_mismatch():
|
|
51
|
+
"""
|
|
52
|
+
>>> import numpy as np
|
|
53
|
+
>>> 3.0
|
|
54
|
+
3
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def dict_not_dict():
|
|
59
|
+
"""
|
|
60
|
+
>>> dict(a=1, b=2)
|
|
61
|
+
['a', 'b']
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def dict_not_dict_2():
|
|
65
|
+
"""
|
|
66
|
+
>>> [('a', 1), ('b', 2)]
|
|
67
|
+
{'a': 1, 'b': 2}
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def dict_wrong_keys():
|
|
72
|
+
"""
|
|
73
|
+
>>> dict(a=1, b=2)
|
|
74
|
+
{'c': 1, 'd': 2}
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def dict_wrong_values():
|
|
79
|
+
"""
|
|
80
|
+
>>> dict(a=1, b=2)
|
|
81
|
+
{'a': -1, 'b': -2}
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def dict_wrong_values_np():
|
|
86
|
+
"""
|
|
87
|
+
>>> import numpy as np
|
|
88
|
+
>>> dict(a=1, b=np.arange(3)/3)
|
|
89
|
+
{'a': 1, 'b': array([0, 0.335, 0.69])}
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def dict_nested_wrong_values_np():
|
|
94
|
+
"""
|
|
95
|
+
>>> import numpy as np
|
|
96
|
+
>>> dict(a=1, b=dict(blurb=np.arange(3)/3))
|
|
97
|
+
{'a': 1, 'b': {'blurb': array([0, 0.335, 0.69])}}
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# This is an XFAIL
|
|
102
|
+
# Currently, checking nested dicts falls back to vanilla doctest
|
|
103
|
+
# When this is fixed, move this case to module_cases.py
|
|
104
|
+
def dict_nested_needs_numeric_comparison():
|
|
105
|
+
"""
|
|
106
|
+
>>> import numpy as np
|
|
107
|
+
>>> dict(a=1, b=dict(blurb=np.arange(3)/3))
|
|
108
|
+
{'a': 1, 'b': {'blurb': array([0, 0.333, 0.667])}}
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# This is an XFAIL
|
|
113
|
+
# Nested sequences which contain strings fall back to vanilla doctest
|
|
114
|
+
def list_of_tuples():
|
|
115
|
+
"""
|
|
116
|
+
>>> [('a', 1/3), ('b', 2/3)]
|
|
117
|
+
[('a', 0.333), ('b', 0.667)]
|
|
118
|
+
"""
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
'func7', 'manip_printoptions', 'array_abbreviation'
|
|
4
|
-
]
|
|
5
|
-
|
|
1
|
+
""" A collection of test cases.
|
|
2
|
+
"""
|
|
6
3
|
|
|
7
4
|
def func():
|
|
8
5
|
"""
|
|
@@ -242,3 +239,38 @@ def array_and_list_2():
|
|
|
242
239
|
>>> [1, 2, 3]
|
|
243
240
|
array([1, 2, 3])
|
|
244
241
|
"""
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def two_dicts():
|
|
245
|
+
"""
|
|
246
|
+
>>> import numpy as np
|
|
247
|
+
>>> dict(a=0, b=1)
|
|
248
|
+
{'a': 0, 'b': 1}
|
|
249
|
+
>>> {'a': 1/3, 'b': 2/3}
|
|
250
|
+
{'a': 0.333, 'b': 0.667}
|
|
251
|
+
>>> {'a': 0., 'b': np.arange(3) / 3 }
|
|
252
|
+
{'a': 0.0, 'b': array([0, 0.333, 0.667])}
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
def nested_dicts():
|
|
256
|
+
# Note that we need to give all digits: checking nested dictionaries
|
|
257
|
+
# currently fall back to vanilla doctest.
|
|
258
|
+
# cf failure_cases.py::dict_nested_needs_numeric_comparison
|
|
259
|
+
"""
|
|
260
|
+
>>> import numpy as np
|
|
261
|
+
>>> {'a': 1.0, 'b': dict(blurb=np.arange(3)/3)}
|
|
262
|
+
{'a': 1.0, 'b': {'blurb': array([0, 0.33333333, 0.66666667])}}
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def list_of_tuples_numeric():
|
|
267
|
+
# cf failure_cases.py::list_of_tuples --- string entries preclude numeric comparisons
|
|
268
|
+
"""
|
|
269
|
+
>>> [(1, 1/3), (2, 2/3)]
|
|
270
|
+
[(1, 0.333), (2, 0.667)]
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# This is used by test_testmod.py::test_public_object_discovery
|
|
275
|
+
# While in test we only need __all__ to be not empty, let's make it correct, too.
|
|
276
|
+
__all__ = [x for x in vars().keys() if not x.startswith("_")]
|
|
@@ -15,6 +15,7 @@ import pytest
|
|
|
15
15
|
|
|
16
16
|
try:
|
|
17
17
|
import scipy # noqa
|
|
18
|
+
from scipy import stats # https://github.com/scipy/scipy_doctest/issues/184
|
|
18
19
|
HAVE_SCIPY = True
|
|
19
20
|
except Exception:
|
|
20
21
|
HAVE_SCIPY = False
|
|
@@ -117,6 +118,29 @@ def test_tuple_and_list():
|
|
|
117
118
|
assert res.failed == 2
|
|
118
119
|
|
|
119
120
|
|
|
121
|
+
def test_list_of_tuples():
|
|
122
|
+
config = DTConfig()
|
|
123
|
+
res, _ = _testmod(failure_cases,
|
|
124
|
+
strategy=[failure_cases.list_of_tuples],
|
|
125
|
+
config=config)
|
|
126
|
+
assert res.failed == 1
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_dict():
|
|
130
|
+
config = DTConfig()
|
|
131
|
+
res, _ = _testmod(failure_cases,
|
|
132
|
+
strategy=[failure_cases.dict_not_dict,
|
|
133
|
+
failure_cases.dict_not_dict_2,
|
|
134
|
+
failure_cases.dict_wrong_keys,
|
|
135
|
+
failure_cases.dict_wrong_values,
|
|
136
|
+
failure_cases.dict_wrong_values_np,
|
|
137
|
+
failure_cases.dict_nested_wrong_values_np,
|
|
138
|
+
failure_cases.dict_nested_needs_numeric_comparison,
|
|
139
|
+
],
|
|
140
|
+
config=config)
|
|
141
|
+
assert res.failed == 7
|
|
142
|
+
|
|
143
|
+
|
|
120
144
|
@pytest.mark.parametrize('strict, num_fails', [(True, 1), (False, 0)])
|
|
121
145
|
class TestStrictDType:
|
|
122
146
|
def test_np_fix(self, strict, num_fails):
|
|
@@ -10,6 +10,10 @@ import tempfile
|
|
|
10
10
|
import inspect
|
|
11
11
|
from contextlib import contextmanager
|
|
12
12
|
|
|
13
|
+
from typing import Sequence
|
|
14
|
+
|
|
15
|
+
from importlib.metadata import version as get_version, PackageNotFoundError
|
|
16
|
+
from packaging.requirements import Requirement
|
|
13
17
|
|
|
14
18
|
@contextmanager
|
|
15
19
|
def matplotlib_make_nongui():
|
|
@@ -255,6 +259,20 @@ def get_public_objects(module, skiplist=None):
|
|
|
255
259
|
return (items, names), failures
|
|
256
260
|
|
|
257
261
|
|
|
262
|
+
def is_req_satisfied(req_strs: str | Sequence[str]) -> bool:
|
|
263
|
+
""" Check if all PEP 508-compliant requirement(s) are satisfied or not.
|
|
264
|
+
"""
|
|
265
|
+
req_strs = [req_strs] if isinstance(req_strs, str) else req_strs
|
|
266
|
+
reqs = [Requirement(req_str) for req_str in req_strs]
|
|
267
|
+
if any(req.marker is not None for req in reqs):
|
|
268
|
+
msg = r"Markers not supported in `pytest_extra_requires`"
|
|
269
|
+
raise NotImplementedError(msg)
|
|
270
|
+
try:
|
|
271
|
+
return all(get_version(req.name) in req.specifier for req in reqs)
|
|
272
|
+
except PackageNotFoundError:
|
|
273
|
+
return False
|
|
274
|
+
|
|
275
|
+
|
|
258
276
|
# XXX: not used ATM
|
|
259
277
|
modules = []
|
|
260
278
|
def generate_log(module, test):
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
__all__ = ['func9', 'func10', 'iterable_length_1', 'iterable_length_2',
|
|
2
|
-
'tuple_and_list_1', 'tuple_and_list_2']
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def func9():
|
|
6
|
-
"""
|
|
7
|
-
Wrong output.
|
|
8
|
-
>>> import numpy as np
|
|
9
|
-
>>> np.array([1, 2, 3])
|
|
10
|
-
array([2, 3, 4])
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def func10():
|
|
15
|
-
"""
|
|
16
|
-
NameError
|
|
17
|
-
>>> import numpy as np
|
|
18
|
-
>>> np.arraY([1, 2, 3])
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def iterable_length_1():
|
|
23
|
-
"""
|
|
24
|
-
>>> [1, 2, 3]
|
|
25
|
-
[1, 2, 3, 4]
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def iterable_length_2():
|
|
30
|
-
"""
|
|
31
|
-
>>> [1, 2, 3]
|
|
32
|
-
[1, 2]
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def tuple_and_list_1():
|
|
37
|
-
"""
|
|
38
|
-
>>> [0, 1, 2]
|
|
39
|
-
(0, 1, 2)
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def tuple_and_list_2():
|
|
44
|
-
"""
|
|
45
|
-
>>> (0, 1, 2)
|
|
46
|
-
[0, 1, 2]
|
|
47
|
-
"""
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def dtype_mismatch():
|
|
51
|
-
"""
|
|
52
|
-
>>> import numpy as np
|
|
53
|
-
>>> 3.0
|
|
54
|
-
3
|
|
55
|
-
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/tests/scipy_ndimage_tutorial_clone.rst
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scipy_doctest-1.7.1 → scipy_doctest-2.0.0}/scipy_doctest/tests/test_pytest_configuration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|