conftest.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708
  1. # Pytest customization
  2. import json
  3. import multiprocessing
  4. import os
  5. import sys
  6. import warnings
  7. import tempfile
  8. from contextlib import contextmanager
  9. from typing import Literal
  10. import numpy as np
  11. import pytest
  12. try:
  13. import hypothesis
  14. hypothesis_available = True
  15. except ImportError:
  16. hypothesis_available = False
  17. from scipy._lib._fpumode import get_fpu_mode
  18. from scipy._lib._array_api import (
  19. SCIPY_ARRAY_API, SCIPY_DEVICE, array_namespace, default_xp,
  20. is_cupy, is_dask, is_jax, is_torch,
  21. )
  22. from scipy._lib._testutils import FPUModeChangeWarning
  23. from scipy._lib.array_api_extra.testing import patch_lazy_xp_functions
  24. from scipy._lib import _pep440
  25. try:
  26. from scipy_doctest.conftest import dt_config
  27. HAVE_SCPDT = True
  28. except ModuleNotFoundError:
  29. HAVE_SCPDT = False
  30. try:
  31. import pytest_run_parallel # noqa:F401
  32. PARALLEL_RUN_AVAILABLE = True
  33. except Exception:
  34. PARALLEL_RUN_AVAILABLE = False
  35. def pytest_configure(config):
  36. """
  37. Add pytest markers to avoid PytestUnknownMarkWarning
  38. This needs to contain all markers that are SciPy-specific, as well as
  39. dummy fallbacks for markers defined in optional test packages.
  40. Note that we need both the registration here *and* in `pytest.ini`.
  41. """
  42. config.addinivalue_line("markers",
  43. "slow: Tests that are very slow.")
  44. config.addinivalue_line("markers",
  45. "xslow: mark test as extremely slow (not run unless explicitly requested)")
  46. config.addinivalue_line("markers",
  47. "xfail_on_32bit: mark test as failing on 32-bit platforms")
  48. config.addinivalue_line("markers",
  49. "array_api_backends: test iterates on all array API backends")
  50. config.addinivalue_line("markers",
  51. ("skip_xp_backends(backends, reason=None, np_only=False, cpu_only=False, " +
  52. "eager_only=False, exceptions=None): mark the desired skip configuration " +
  53. "for the `skip_xp_backends` fixture"))
  54. config.addinivalue_line("markers",
  55. ("xfail_xp_backends(backends, reason=None, np_only=False, cpu_only=False, " +
  56. "eager_only=False, exceptions=None): mark the desired xfail configuration " +
  57. "for the `xfail_xp_backends` fixture"))
  58. try:
  59. import pytest_timeout # noqa:F401
  60. except Exception:
  61. config.addinivalue_line(
  62. "markers", 'timeout: mark a test for a non-default timeout')
  63. try:
  64. # This is a more reliable test of whether pytest_fail_slow is installed
  65. # When I uninstalled it, `import pytest_fail_slow` didn't fail!
  66. from pytest_fail_slow import parse_duration # type: ignore[import-not-found] # noqa:F401,E501
  67. except Exception:
  68. config.addinivalue_line(
  69. "markers", 'fail_slow: mark a test for a non-default timeout failure')
  70. if not PARALLEL_RUN_AVAILABLE:
  71. config.addinivalue_line(
  72. 'markers',
  73. 'parallel_threads_limit(n): run the given test function in parallel '
  74. 'using `n` threads.')
  75. config.addinivalue_line(
  76. "markers",
  77. "thread_unsafe: mark the test function as single-threaded",
  78. )
  79. config.addinivalue_line(
  80. "markers",
  81. "iterations(n): run the given test function `n` times in each thread",
  82. )
  83. if os.name == 'posix' and sys.version_info < (3, 14):
  84. # On POSIX, Python 3.13 and older uses the 'fork' context by
  85. # default. Calling fork() from multiple threads leads to
  86. # deadlocks. This has been changed in 3.14 to 'forkserver'.
  87. multiprocessing.set_start_method('forkserver', force=True)
  88. def pytest_runtest_setup(item):
  89. mark = item.get_closest_marker("xslow")
  90. if mark is not None:
  91. try:
  92. v = int(os.environ.get('SCIPY_XSLOW', '0'))
  93. except ValueError:
  94. v = False
  95. if not v:
  96. pytest.skip("very slow test; "
  97. "set environment variable SCIPY_XSLOW=1 to run it")
  98. mark = item.get_closest_marker("xfail_on_32bit")
  99. if mark is not None and np.intp(0).itemsize < 8:
  100. pytest.xfail(f'Fails on our 32-bit test platform(s): {mark.args[0]}')
  101. # Older versions of threadpoolctl have an issue that may lead to this
  102. # warning being emitted, see gh-14441
  103. with warnings.catch_warnings():
  104. warnings.simplefilter("ignore", pytest.PytestUnraisableExceptionWarning)
  105. try:
  106. from threadpoolctl import threadpool_limits
  107. HAS_THREADPOOLCTL = True
  108. except Exception: # observed in gh-14441: (ImportError, AttributeError)
  109. # Optional dependency only. All exceptions are caught, for robustness
  110. HAS_THREADPOOLCTL = False
  111. if HAS_THREADPOOLCTL:
  112. # Set the number of openmp threads based on the number of workers
  113. # xdist is using to prevent oversubscription. Simplified version of what
  114. # sklearn does (it can rely on threadpoolctl and its builtin OpenMP helper
  115. # functions)
  116. try:
  117. xdist_worker_count = int(os.environ['PYTEST_XDIST_WORKER_COUNT'])
  118. except KeyError:
  119. # raises when pytest-xdist is not installed
  120. return
  121. if not os.getenv('OMP_NUM_THREADS'):
  122. max_openmp_threads = os.cpu_count() // 2 # use nr of physical cores
  123. threads_per_worker = max(max_openmp_threads // xdist_worker_count, 1)
  124. try:
  125. threadpool_limits(threads_per_worker, user_api='blas')
  126. except Exception:
  127. # May raise AttributeError for older versions of OpenBLAS.
  128. # Catch any error for robustness.
  129. return
  130. @pytest.fixture(scope="function", autouse=True)
  131. def check_fpu_mode(request):
  132. """
  133. Check FPU mode was not changed during the test.
  134. """
  135. old_mode = get_fpu_mode()
  136. yield
  137. new_mode = get_fpu_mode()
  138. if old_mode != new_mode:
  139. warnings.warn(f"FPU mode changed from {old_mode:#x} to {new_mode:#x} during "
  140. "the test",
  141. category=FPUModeChangeWarning, stacklevel=0)
  142. if not PARALLEL_RUN_AVAILABLE:
  143. @pytest.fixture
  144. def num_parallel_threads():
  145. return 1
  146. # Array API backend handling
  147. xp_known_backends = {'numpy', 'array_api_strict', 'torch', 'cupy', 'jax.numpy',
  148. 'dask.array'}
  149. xp_available_backends = [
  150. pytest.param(np, id='numpy', marks=pytest.mark.array_api_backends)
  151. ]
  152. xp_skip_cpu_only_backends = set()
  153. xp_skip_eager_only_backends = set()
  154. if SCIPY_ARRAY_API:
  155. # fill the dict of backends with available libraries
  156. try:
  157. import array_api_strict
  158. xp_available_backends.append(
  159. pytest.param(array_api_strict, id='array_api_strict',
  160. marks=pytest.mark.array_api_backends))
  161. if _pep440.parse(array_api_strict.__version__) < _pep440.Version('2.3'):
  162. raise ImportError("array-api-strict must be >= version 2.3")
  163. array_api_strict.set_array_api_strict_flags(
  164. api_version='2024.12'
  165. )
  166. except ImportError:
  167. pass
  168. try:
  169. import torch # type: ignore[import-not-found]
  170. xp_available_backends.append(
  171. pytest.param(torch, id='torch',
  172. marks=pytest.mark.array_api_backends))
  173. torch.set_default_device(SCIPY_DEVICE)
  174. if SCIPY_DEVICE != "cpu":
  175. xp_skip_cpu_only_backends.add('torch')
  176. # default to float64 unless explicitly requested
  177. default = os.getenv('SCIPY_DEFAULT_DTYPE', default='float64')
  178. if default == 'float64':
  179. torch.set_default_dtype(torch.float64)
  180. elif default != "float32":
  181. raise ValueError(
  182. "SCIPY_DEFAULT_DTYPE env var, if set, can only be either 'float64' "
  183. f"or 'float32'. Got '{default}' instead."
  184. )
  185. except ImportError:
  186. pass
  187. try:
  188. import cupy # type: ignore[import-not-found]
  189. # Note: cupy disregards SCIPY_DEVICE and always runs on cuda.
  190. # It will fail to import if you don't have CUDA hardware and drivers.
  191. xp_available_backends.append(
  192. pytest.param(cupy, id='cupy',
  193. marks=pytest.mark.array_api_backends))
  194. xp_skip_cpu_only_backends.add('cupy')
  195. # this is annoying in CuPy 13.x
  196. warnings.filterwarnings(
  197. 'ignore', 'cupyx.jit.rawkernel is experimental', category=FutureWarning
  198. )
  199. from cupyx.scipy import signal
  200. del signal
  201. except ImportError:
  202. pass
  203. try:
  204. import jax.numpy # type: ignore[import-not-found]
  205. xp_available_backends.append(
  206. pytest.param(jax.numpy, id='jax.numpy',
  207. marks=[pytest.mark.array_api_backends,
  208. # Uses xpx.testing.patch_lazy_xp_functions to monkey-patch module
  209. pytest.mark.thread_unsafe]))
  210. jax.config.update("jax_enable_x64", True)
  211. jax.config.update("jax_default_device", jax.devices(SCIPY_DEVICE)[0])
  212. if SCIPY_DEVICE != "cpu":
  213. xp_skip_cpu_only_backends.add('jax.numpy')
  214. # JAX can be eager or lazy (when wrapped in jax.jit). However it is
  215. # recommended by upstream devs to assume it's always lazy.
  216. xp_skip_eager_only_backends.add('jax.numpy')
  217. except ImportError:
  218. pass
  219. try:
  220. import dask.array as da
  221. xp_available_backends.append(
  222. pytest.param(da, id='dask.array',
  223. marks=[pytest.mark.array_api_backends,
  224. # Uses xpx.testing.patch_lazy_xp_functions to monkey-patch module
  225. pytest.mark.thread_unsafe]))
  226. # Dask can wrap around cupy. However, this is untested in scipy
  227. # (and will almost surely not work as delegation will misbehave).
  228. # Dask, strictly speaking, can be eager, in the sense that
  229. # __array__, __bool__ etc. are implemented and do not raise.
  230. # However, calling them triggers an extra computation of the whole graph
  231. # until that point, which is highly destructive for performance.
  232. xp_skip_eager_only_backends.add('dask.array')
  233. except ImportError:
  234. pass
  235. xp_available_backend_ids = {p.id for p in xp_available_backends}
  236. assert not xp_available_backend_ids - xp_known_backends
  237. # by default, use all available backends
  238. if (
  239. isinstance(SCIPY_ARRAY_API, str)
  240. and SCIPY_ARRAY_API.lower() not in ("1", "true", "all")
  241. ):
  242. SCIPY_ARRAY_API_ = set(json.loads(SCIPY_ARRAY_API))
  243. if SCIPY_ARRAY_API_ != {'all'}:
  244. if SCIPY_ARRAY_API_ - xp_available_backend_ids:
  245. msg = ("'--array-api-backend' must be in "
  246. f"{xp_available_backend_ids}; got {SCIPY_ARRAY_API_}")
  247. raise ValueError(msg)
  248. # Only select a subset of backends
  249. xp_available_backends = [
  250. param for param in xp_available_backends
  251. if param.id in SCIPY_ARRAY_API_
  252. ]
  253. @pytest.fixture(params=xp_available_backends)
  254. def xp(request):
  255. """Run the test that uses this fixture on each available array API library.
  256. You can select all and only the tests that use the `xp` fixture by
  257. passing `-m array_api_backends` to pytest.
  258. You can select where individual tests run through the `@skip_xp_backends`,
  259. `@xfail_xp_backends`, and `@skip_xp_invalid_arg` pytest markers.
  260. Please read: https://docs.scipy.org/doc/scipy/dev/api-dev/array_api.html#adding-tests
  261. """
  262. # Read all @pytest.marks.skip_xp_backends markers that decorate to the test,
  263. # if any, and raise pytest.skip() if the current xp is in the list.
  264. skip_or_xfail_xp_backends(request, "skip")
  265. # Read all @pytest.marks.xfail_xp_backends markers that decorate the test,
  266. # if any, and raise pytest.xfail() if the current xp is in the list.
  267. skip_or_xfail_xp_backends(request, "xfail")
  268. xp = request.param
  269. # Potentially wrap namespace with array_api_compat
  270. xp = array_namespace(xp.empty(0))
  271. if SCIPY_ARRAY_API:
  272. # If xp==jax.numpy, wrap tested functions in jax.jit
  273. # If xp==dask.array, wrap tested functions to test that graph is not computed
  274. with patch_lazy_xp_functions(request=request, xp=request.param):
  275. # Throughout all calls to assert_almost_equal, assert_array_almost_equal,
  276. # and xp_assert_* functions, test that the array namespace is xp in both
  277. # the expected and actual arrays. This is to detect the case where both
  278. # arrays are erroneously just plain numpy while xp is something else.
  279. with default_xp(xp):
  280. yield xp
  281. else:
  282. yield xp
  283. skip_xp_invalid_arg = pytest.mark.skipif(SCIPY_ARRAY_API,
  284. reason = ('Test involves masked arrays, object arrays, or other types '
  285. 'that are not valid input when `SCIPY_ARRAY_API` is used.'))
  286. def _backends_kwargs_from_request(request, skip_or_xfail):
  287. """A helper for {skip,xfail}_xp_backends.
  288. Return dict of {backend to skip/xfail: top reason to skip/xfail it}
  289. """
  290. markers = list(request.node.iter_markers(f'{skip_or_xfail}_xp_backends'))
  291. reasons = {backend: [] for backend in xp_known_backends}
  292. for marker in markers:
  293. invalid_kwargs = set(marker.kwargs) - {
  294. "cpu_only", "np_only", "eager_only", "reason", "exceptions"}
  295. if invalid_kwargs:
  296. raise TypeError(f"Invalid kwargs: {invalid_kwargs}")
  297. exceptions = set(marker.kwargs.get('exceptions', []))
  298. invalid_exceptions = exceptions - xp_known_backends
  299. if (invalid_exceptions := list(exceptions - xp_known_backends)):
  300. raise ValueError(f"Unknown backend(s): {invalid_exceptions}; "
  301. f"must be a subset of {list(xp_known_backends)}")
  302. if marker.kwargs.get('np_only', False):
  303. reason = marker.kwargs.get("reason") or "do not run with non-NumPy backends"
  304. for backend, backend_reasons in reasons.items():
  305. if backend != 'numpy' and backend not in exceptions:
  306. backend_reasons.append(reason)
  307. elif marker.kwargs.get('cpu_only', False):
  308. reason = marker.kwargs.get("reason") or (
  309. "no array-agnostic implementation or delegation available "
  310. "for this backend and device")
  311. for backend in xp_skip_cpu_only_backends - exceptions:
  312. reasons[backend].append(reason)
  313. elif marker.kwargs.get('eager_only', False):
  314. reason = marker.kwargs.get("reason") or (
  315. "eager checks not executed on lazy backends")
  316. for backend in xp_skip_eager_only_backends - exceptions:
  317. reasons[backend].append(reason)
  318. # add backends, if any
  319. if len(marker.args) == 1:
  320. backend = marker.args[0]
  321. if backend not in xp_known_backends:
  322. raise ValueError(f"Unknown backend: {backend}; "
  323. f"must be one of {list(xp_known_backends)}")
  324. reason = marker.kwargs.get("reason") or (
  325. f"do not run with array API backend: {backend}")
  326. # reason overrides the ones from cpu_only, np_only, and eager_only.
  327. # This is regardless of order of appearence of the markers.
  328. reasons[backend].insert(0, reason)
  329. for kwarg in ("cpu_only", "np_only", "eager_only", "exceptions"):
  330. if kwarg in marker.kwargs:
  331. raise ValueError(f"{kwarg} is mutually exclusive with {backend}")
  332. elif len(marker.args) > 1:
  333. raise ValueError(
  334. f"Please specify only one backend per marker: {marker.args}"
  335. )
  336. return {backend: backend_reasons[0]
  337. for backend, backend_reasons in reasons.items()
  338. if backend_reasons}
  339. def skip_or_xfail_xp_backends(request: pytest.FixtureRequest,
  340. skip_or_xfail: Literal['skip', 'xfail']) -> None:
  341. """
  342. Helper of the `xp` fixture.
  343. Skip or xfail based on the ``skip_xp_backends`` or ``xfail_xp_backends`` markers.
  344. See the "Support for the array API standard" docs page for usage examples.
  345. Usage
  346. -----
  347. ::
  348. skip_xp_backends = pytest.mark.skip_xp_backends
  349. xfail_xp_backends = pytest.mark.xfail_xp_backends
  350. ...
  351. @skip_xp_backends(backend, *, reason=None)
  352. @skip_xp_backends(*, cpu_only=True, exceptions=(), reason=None)
  353. @skip_xp_backends(*, eager_only=True, exceptions=(), reason=None)
  354. @skip_xp_backends(*, np_only=True, exceptions=(), reason=None)
  355. @xfail_xp_backends(backend, *, reason=None)
  356. @xfail_xp_backends(*, cpu_only=True, exceptions=(), reason=None)
  357. @xfail_xp_backends(*, eager_only=True, exceptions=(), reason=None)
  358. @xfail_xp_backends(*, np_only=True, exceptions=(), reason=None)
  359. Parameters
  360. ----------
  361. backend : str, optional
  362. Backend to skip/xfail, e.g. ``"torch"``.
  363. Mutually exclusive with ``cpu_only``, ``eager_only``, and ``np_only``.
  364. cpu_only : bool, optional
  365. When ``True``, the test is skipped/xfailed on non-CPU devices,
  366. minus exceptions. Mutually exclusive with ``backend``.
  367. eager_only : bool, optional
  368. When ``True``, the test is skipped/xfailed for lazy backends, e.g. those
  369. with major caveats when invoking ``__array__``, ``__bool__``, ``__float__``,
  370. or ``__complex__``, minus exceptions. Mutually exclusive with ``backend``.
  371. np_only : bool, optional
  372. When ``True``, the test is skipped/xfailed for all backends other
  373. than the default NumPy backend and the exceptions.
  374. Mutually exclusive with ``backend``. Implies ``cpu_only`` and ``eager_only``.
  375. reason : str, optional
  376. A reason for the skip/xfail. If omitted, a default reason is used.
  377. exceptions : list[str], optional
  378. A list of exceptions for use with ``cpu_only``, ``eager_only``, or ``np_only``.
  379. This should be provided when delegation is implemented for some,
  380. but not all, non-CPU/non-NumPy backends.
  381. """
  382. if f"{skip_or_xfail}_xp_backends" not in request.keywords:
  383. return
  384. skip_xfail_reasons = _backends_kwargs_from_request(
  385. request, skip_or_xfail=skip_or_xfail
  386. )
  387. xp = request.param
  388. if xp.__name__ in skip_xfail_reasons:
  389. reason = skip_xfail_reasons[xp.__name__]
  390. assert reason # Default reason applied above
  391. skip_or_xfail = getattr(pytest, skip_or_xfail)
  392. skip_or_xfail(reason=reason)
  393. @pytest.fixture
  394. def devices(xp):
  395. """Fixture that returns a list of all devices for the backend, plus None.
  396. Used to test input->output device propagation.
  397. Usage
  398. -----
  399. from scipy._lib._array_api import xp_device
  400. def test_device(xp, devices):
  401. for d in devices:
  402. x = xp.asarray(..., device=d)
  403. y = f(x)
  404. assert xp_device(y) == xp_device(x)
  405. """
  406. if is_cupy(xp):
  407. # CuPy does not support devices other than the current one
  408. # data-apis/array-api-compat#293
  409. pytest.xfail(reason="data-apis/array-api-compat#293")
  410. if is_dask(xp):
  411. # Skip dummy DASK_DEVICE from array-api-compat, which does not propagate
  412. return ["cpu", None]
  413. if is_jax(xp):
  414. # The .device attribute is not accessible inside jax.jit; the consequence
  415. # (downstream of array-api-compat hacks) is that a non-default device in
  416. # input is not guaranteed to propagate to the output even if the scipy code
  417. # states `device=xp_device(arg)`` in all array creation functions.
  418. # While this issue is specific to jax.jit, it would be unnecessarily
  419. # verbose to skip the test for each jit-capable function and run it for
  420. # those that only support eager mode.
  421. pytest.xfail(reason="jax-ml/jax#26000")
  422. if is_torch(xp):
  423. devices = xp.__array_namespace_info__().devices()
  424. # open an issue about this - cannot branch based on `any`/`all`?
  425. return (device for device in devices if device.type != 'meta')
  426. return xp.__array_namespace_info__().devices() + [None]
  427. if hypothesis_available:
  428. # Following the approach of NumPy's conftest.py...
  429. # Use a known and persistent tmpdir for hypothesis' caches, which
  430. # can be automatically cleared by the OS or user.
  431. hypothesis.configuration.set_hypothesis_home_dir(
  432. os.path.join(tempfile.gettempdir(), ".hypothesis")
  433. )
  434. # We register two custom profiles for SciPy - for details see
  435. # https://hypothesis.readthedocs.io/en/latest/settings.html
  436. # The first is designed for our own CI runs; the latter also
  437. # forces determinism and is designed for use via scipy.test()
  438. hypothesis.settings.register_profile(
  439. name="nondeterministic", deadline=None, print_blob=True,
  440. )
  441. hypothesis.settings.register_profile(
  442. name="deterministic",
  443. deadline=None, print_blob=True, database=None, derandomize=True,
  444. suppress_health_check=list(hypothesis.HealthCheck),
  445. )
  446. # Profile is currently set by environment variable `SCIPY_HYPOTHESIS_PROFILE`
  447. # In the future, it would be good to work the choice into `.spin/cmds.py`.
  448. SCIPY_HYPOTHESIS_PROFILE = os.environ.get("SCIPY_HYPOTHESIS_PROFILE",
  449. "deterministic")
  450. hypothesis.settings.load_profile(SCIPY_HYPOTHESIS_PROFILE)
  451. ############################################################################
  452. # doctesting stuff
  453. if HAVE_SCPDT:
  454. # FIXME: populate the dict once
  455. @contextmanager
  456. def warnings_errors_and_rng(test=None):
  457. """Temporarily turn (almost) all warnings to errors.
  458. Filter out known warnings which we allow.
  459. """
  460. known_warnings = dict()
  461. # these functions are known to emit "divide by zero" RuntimeWarnings
  462. divide_by_zero = [
  463. 'scipy.linalg.norm', 'scipy.ndimage.center_of_mass',
  464. ]
  465. for name in divide_by_zero:
  466. known_warnings[name] = dict(category=RuntimeWarning,
  467. message='divide by zero')
  468. # Deprecated stuff
  469. deprecated = []
  470. for name in deprecated:
  471. known_warnings[name] = dict(category=DeprecationWarning)
  472. from scipy import integrate
  473. # the functions are known to emit IntegrationWarnings
  474. integration_w = ['scipy.special.ellip_normal',
  475. 'scipy.special.ellip_harm_2',
  476. ]
  477. for name in integration_w:
  478. known_warnings[name] = dict(category=integrate.IntegrationWarning,
  479. message='The occurrence of roundoff')
  480. # scipy.stats deliberately emits UserWarnings sometimes
  481. user_w = ['scipy.stats.anderson_ksamp', 'scipy.stats.kurtosistest',
  482. 'scipy.stats.normaltest', 'scipy.sparse.linalg.norm']
  483. for name in user_w:
  484. known_warnings[name] = dict(category=UserWarning)
  485. # additional one-off warnings to filter
  486. dct = {
  487. 'scipy.sparse.linalg.norm':
  488. dict(category=UserWarning, message="Exited at iteration"),
  489. # tutorials
  490. 'linalg.rst':
  491. dict(message='the matrix subclass is not',
  492. category=PendingDeprecationWarning),
  493. 'stats.rst':
  494. dict(message='The maximum number of subdivisions',
  495. category=integrate.IntegrationWarning),
  496. }
  497. known_warnings.update(dct)
  498. # these legitimately emit warnings in examples
  499. legit = set('scipy.signal.normalize')
  500. # Now, the meat of the matter: filter warnings,
  501. # also control the random seed for each doctest.
  502. # XXX: this matches the refguide-check behavior, but is a tad strange:
  503. # makes sure that the seed the old-fashioned np.random* methods is
  504. # *NOT* reproducible but the new-style `default_rng()` *IS* repoducible.
  505. # Should these two be either both repro or both not repro?
  506. from scipy._lib._util import _fixed_default_rng
  507. import numpy as np
  508. with _fixed_default_rng():
  509. np.random.seed(None)
  510. with warnings.catch_warnings():
  511. if test and test.name in known_warnings:
  512. warnings.filterwarnings('ignore', **known_warnings[test.name])
  513. yield
  514. elif test and test.name in legit:
  515. yield
  516. else:
  517. warnings.simplefilter('error', Warning)
  518. warnings.filterwarnings('ignore', ".*odr.*", DeprecationWarning)
  519. yield
  520. dt_config.user_context_mgr = warnings_errors_and_rng
  521. dt_config.skiplist = set([
  522. 'scipy.linalg.LinAlgError', # comes from numpy
  523. 'scipy.fftpack.fftshift', # fftpack stuff is also from numpy
  524. 'scipy.fftpack.ifftshift',
  525. 'scipy.fftpack.fftfreq',
  526. 'scipy.special.sinc', # sinc is from numpy
  527. 'scipy.optimize.show_options', # does not have much to doctest
  528. 'scipy.signal.normalize', # manipulates warnings (XXX temp skip)
  529. 'scipy.sparse.linalg.norm', # XXX temp skip
  530. # these below test things which inherit from np.ndarray
  531. # cross-ref https://github.com/numpy/numpy/issues/28019
  532. 'scipy.io.matlab.MatlabObject.strides',
  533. 'scipy.io.matlab.MatlabObject.dtype',
  534. 'scipy.io.matlab.MatlabOpaque.dtype',
  535. 'scipy.io.matlab.MatlabOpaque.strides',
  536. 'scipy.io.matlab.MatlabFunction.strides',
  537. 'scipy.io.matlab.MatlabFunction.dtype'
  538. ])
  539. # these are affected by NumPy 2.0 scalar repr: rely on string comparison
  540. if np.__version__ < "2":
  541. dt_config.skiplist.update(set([
  542. 'scipy.io.hb_read',
  543. 'scipy.io.hb_write',
  544. 'scipy.sparse.csgraph.connected_components',
  545. 'scipy.sparse.csgraph.depth_first_order',
  546. 'scipy.sparse.csgraph.shortest_path',
  547. 'scipy.sparse.csgraph.floyd_warshall',
  548. 'scipy.sparse.csgraph.dijkstra',
  549. 'scipy.sparse.csgraph.bellman_ford',
  550. 'scipy.sparse.csgraph.johnson',
  551. 'scipy.sparse.csgraph.yen',
  552. 'scipy.sparse.csgraph.breadth_first_order',
  553. 'scipy.sparse.csgraph.reverse_cuthill_mckee',
  554. 'scipy.sparse.csgraph.structural_rank',
  555. 'scipy.sparse.csgraph.construct_dist_matrix',
  556. 'scipy.sparse.csgraph.reconstruct_path',
  557. 'scipy.ndimage.value_indices',
  558. 'scipy.stats.mstats.describe',
  559. ]))
  560. # help pytest collection a bit: these names are either private
  561. # (distributions), or just do not need doctesting.
  562. dt_config.pytest_extra_ignore = [
  563. "scipy.stats.distributions",
  564. "scipy.optimize.cython_optimize",
  565. "scipy.test",
  566. "scipy.show_config",
  567. # equivalent to "pytest --ignore=path/to/file"
  568. "scipy/special/_precompute",
  569. "scipy/interpolate/_interpnd_info.py",
  570. "scipy/interpolate/_rbfinterp_pythran.py",
  571. "scipy/_build_utils/tempita.py",
  572. "scipy/_lib/array_api_compat",
  573. "scipy/_lib/highs",
  574. "scipy/_lib/unuran",
  575. "scipy/_lib/_gcutils.py",
  576. "scipy/_lib/doccer.py",
  577. "scipy/_lib/_uarray",
  578. "scipy/linalg/_cython_signature_generator.py",
  579. "scipy/linalg/_generate_pyx.py",
  580. "scipy/linalg/_linalg_pythran.py",
  581. "scipy/linalg/_matfuncs_sqrtm_triu.py",
  582. "scipy/ndimage/utils/generate_label_testvectors.py",
  583. "scipy/optimize/_group_columns.py",
  584. "scipy/optimize/_max_len_seq_inner.py",
  585. "scipy/signal/_max_len_seq_inner.py",
  586. "scipy/sparse/_generate_sparsetools.py",
  587. "scipy/special/_generate_pyx.py",
  588. "scipy/stats/_stats_pythran.py",
  589. ]
  590. dt_config.pytest_extra_xfail = {
  591. # name: reason
  592. "ND_regular_grid.rst": "ReST parser limitation",
  593. "extrapolation_examples.rst": "ReST parser limitation",
  594. "sampling_pinv.rst": "__cinit__ unexpected argument",
  595. "sampling_srou.rst": "nan in scalar_power",
  596. "probability_distributions.rst": "integration warning",
  597. }
  598. # tutorials
  599. dt_config.pseudocode = set(['integrate.nquad(func,'])
  600. dt_config.local_resources = {
  601. 'io.rst': [
  602. "octave_a.mat",
  603. "octave_cells.mat",
  604. "octave_struct.mat"
  605. ]
  606. }
  607. dt_config.strict_check = True
  608. # ignore Matplotlib's `ax.text`:
  609. dt_config.stopwords.add('.text(')
  610. ############################################################################