3RNN/Lib/site-packages/scipy/_lib/tests/test_public_api.py

492 lines
19 KiB
Python
Raw Normal View History

2024-05-26 19:49:15 +02:00
"""
This test script is adopted from:
https://github.com/numpy/numpy/blob/main/numpy/tests/test_public_api.py
"""
import pkgutil
import types
import importlib
import warnings
from importlib import import_module
import pytest
import scipy
from scipy.conftest import xp_available_backends
def test_dir_testing():
"""Assert that output of dir has only one "testing/tester"
attribute without duplicate"""
assert len(dir(scipy)) == len(set(dir(scipy)))
# Historically SciPy has not used leading underscores for private submodules
# much. This has resulted in lots of things that look like public modules
# (i.e. things that can be imported as `import scipy.somesubmodule.somefile`),
# but were never intended to be public. The PUBLIC_MODULES list contains
# modules that are either public because they were meant to be, or because they
# contain public functions/objects that aren't present in any other namespace
# for whatever reason and therefore should be treated as public.
PUBLIC_MODULES = ["scipy." + s for s in [
"cluster",
"cluster.vq",
"cluster.hierarchy",
"constants",
"datasets",
"fft",
"fftpack",
"integrate",
"interpolate",
"io",
"io.arff",
"io.matlab",
"io.wavfile",
"linalg",
"linalg.blas",
"linalg.cython_blas",
"linalg.lapack",
"linalg.cython_lapack",
"linalg.interpolative",
"misc",
"ndimage",
"odr",
"optimize",
"signal",
"signal.windows",
"sparse",
"sparse.linalg",
"sparse.csgraph",
"spatial",
"spatial.distance",
"spatial.transform",
"special",
"stats",
"stats.contingency",
"stats.distributions",
"stats.mstats",
"stats.qmc",
"stats.sampling"
]]
# The PRIVATE_BUT_PRESENT_MODULES list contains modules that lacked underscores
# in their name and hence looked public, but weren't meant to be. All these
# namespace were deprecated in the 1.8.0 release - see "clear split between
# public and private API" in the 1.8.0 release notes.
# These private modules support will be removed in SciPy v2.0.0, as the
# deprecation messages emitted by each of these modules say.
PRIVATE_BUT_PRESENT_MODULES = [
'scipy.constants.codata',
'scipy.constants.constants',
'scipy.fftpack.basic',
'scipy.fftpack.convolve',
'scipy.fftpack.helper',
'scipy.fftpack.pseudo_diffs',
'scipy.fftpack.realtransforms',
'scipy.integrate.dop',
'scipy.integrate.lsoda',
'scipy.integrate.odepack',
'scipy.integrate.quadpack',
'scipy.integrate.vode',
'scipy.interpolate.dfitpack',
'scipy.interpolate.fitpack',
'scipy.interpolate.fitpack2',
'scipy.interpolate.interpnd',
'scipy.interpolate.interpolate',
'scipy.interpolate.ndgriddata',
'scipy.interpolate.polyint',
'scipy.interpolate.rbf',
'scipy.io.arff.arffread',
'scipy.io.harwell_boeing',
'scipy.io.idl',
'scipy.io.matlab.byteordercodes',
'scipy.io.matlab.mio',
'scipy.io.matlab.mio4',
'scipy.io.matlab.mio5',
'scipy.io.matlab.mio5_params',
'scipy.io.matlab.mio5_utils',
'scipy.io.matlab.mio_utils',
'scipy.io.matlab.miobase',
'scipy.io.matlab.streams',
'scipy.io.mmio',
'scipy.io.netcdf',
'scipy.linalg.basic',
'scipy.linalg.decomp',
'scipy.linalg.decomp_cholesky',
'scipy.linalg.decomp_lu',
'scipy.linalg.decomp_qr',
'scipy.linalg.decomp_schur',
'scipy.linalg.decomp_svd',
'scipy.linalg.matfuncs',
'scipy.linalg.misc',
'scipy.linalg.special_matrices',
'scipy.misc.common',
'scipy.misc.doccer',
'scipy.ndimage.filters',
'scipy.ndimage.fourier',
'scipy.ndimage.interpolation',
'scipy.ndimage.measurements',
'scipy.ndimage.morphology',
'scipy.odr.models',
'scipy.odr.odrpack',
'scipy.optimize.cobyla',
'scipy.optimize.cython_optimize',
'scipy.optimize.lbfgsb',
'scipy.optimize.linesearch',
'scipy.optimize.minpack',
'scipy.optimize.minpack2',
'scipy.optimize.moduleTNC',
'scipy.optimize.nonlin',
'scipy.optimize.optimize',
'scipy.optimize.slsqp',
'scipy.optimize.tnc',
'scipy.optimize.zeros',
'scipy.signal.bsplines',
'scipy.signal.filter_design',
'scipy.signal.fir_filter_design',
'scipy.signal.lti_conversion',
'scipy.signal.ltisys',
'scipy.signal.signaltools',
'scipy.signal.spectral',
'scipy.signal.spline',
'scipy.signal.waveforms',
'scipy.signal.wavelets',
'scipy.signal.windows.windows',
'scipy.sparse.base',
'scipy.sparse.bsr',
'scipy.sparse.compressed',
'scipy.sparse.construct',
'scipy.sparse.coo',
'scipy.sparse.csc',
'scipy.sparse.csr',
'scipy.sparse.data',
'scipy.sparse.dia',
'scipy.sparse.dok',
'scipy.sparse.extract',
'scipy.sparse.lil',
'scipy.sparse.linalg.dsolve',
'scipy.sparse.linalg.eigen',
'scipy.sparse.linalg.interface',
'scipy.sparse.linalg.isolve',
'scipy.sparse.linalg.matfuncs',
'scipy.sparse.sparsetools',
'scipy.sparse.spfuncs',
'scipy.sparse.sputils',
'scipy.spatial.ckdtree',
'scipy.spatial.kdtree',
'scipy.spatial.qhull',
'scipy.spatial.transform.rotation',
'scipy.special.add_newdocs',
'scipy.special.basic',
'scipy.special.cython_special',
'scipy.special.orthogonal',
'scipy.special.sf_error',
'scipy.special.specfun',
'scipy.special.spfun_stats',
'scipy.stats.biasedurn',
'scipy.stats.kde',
'scipy.stats.morestats',
'scipy.stats.mstats_basic',
'scipy.stats.mstats_extras',
'scipy.stats.mvn',
'scipy.stats.stats',
]
def is_unexpected(name):
"""Check if this needs to be considered."""
if '._' in name or '.tests' in name or '.setup' in name:
return False
if name in PUBLIC_MODULES:
return False
if name in PRIVATE_BUT_PRESENT_MODULES:
return False
return True
SKIP_LIST = [
'scipy.conftest',
'scipy.version',
]
# XXX: this test does more than it says on the tin - in using `pkgutil.walk_packages`,
# it will raise if it encounters any exceptions which are not handled by `ignore_errors`
# while attempting to import each discovered package.
# For now, `ignore_errors` only ignores what is necessary, but this could be expanded -
# for example, to all errors from private modules or git subpackages - if desired.
def test_all_modules_are_expected():
"""
Test that we don't add anything that looks like a new public module by
accident. Check is based on filenames.
"""
def ignore_errors(name):
# if versions of other array libraries are installed which are incompatible
# with the installed NumPy version, there can be errors on importing
# `array_api_compat`. This should only raise if SciPy is configured with
# that library as an available backend.
for backend, dir_name in {'cupy': 'cupy', 'pytorch': 'torch'}.items():
path = f'array_api_compat.{dir_name}'
if path in name and backend not in xp_available_backends:
return
raise
modnames = []
for _, modname, _ in pkgutil.walk_packages(path=scipy.__path__,
prefix=scipy.__name__ + '.',
onerror=ignore_errors):
if is_unexpected(modname) and modname not in SKIP_LIST:
# We have a name that is new. If that's on purpose, add it to
# PUBLIC_MODULES. We don't expect to have to add anything to
# PRIVATE_BUT_PRESENT_MODULES. Use an underscore in the name!
modnames.append(modname)
if modnames:
raise AssertionError(f'Found unexpected modules: {modnames}')
# Stuff that clearly shouldn't be in the API and is detected by the next test
# below
SKIP_LIST_2 = [
'scipy.char',
'scipy.rec',
'scipy.emath',
'scipy.math',
'scipy.random',
'scipy.ctypeslib',
'scipy.ma'
]
def test_all_modules_are_expected_2():
"""
Method checking all objects. The pkgutil-based method in
`test_all_modules_are_expected` does not catch imports into a namespace,
only filenames.
"""
def find_unexpected_members(mod_name):
members = []
module = importlib.import_module(mod_name)
if hasattr(module, '__all__'):
objnames = module.__all__
else:
objnames = dir(module)
for objname in objnames:
if not objname.startswith('_'):
fullobjname = mod_name + '.' + objname
if isinstance(getattr(module, objname), types.ModuleType):
if is_unexpected(fullobjname) and fullobjname not in SKIP_LIST_2:
members.append(fullobjname)
return members
unexpected_members = find_unexpected_members("scipy")
for modname in PUBLIC_MODULES:
unexpected_members.extend(find_unexpected_members(modname))
if unexpected_members:
raise AssertionError("Found unexpected object(s) that look like "
f"modules: {unexpected_members}")
def test_api_importable():
"""
Check that all submodules listed higher up in this file can be imported
Note that if a PRIVATE_BUT_PRESENT_MODULES entry goes missing, it may
simply need to be removed from the list (deprecation may or may not be
needed - apply common sense).
"""
def check_importable(module_name):
try:
importlib.import_module(module_name)
except (ImportError, AttributeError):
return False
return True
module_names = []
for module_name in PUBLIC_MODULES:
if not check_importable(module_name):
module_names.append(module_name)
if module_names:
raise AssertionError("Modules in the public API that cannot be "
f"imported: {module_names}")
with warnings.catch_warnings(record=True):
warnings.filterwarnings('always', category=DeprecationWarning)
warnings.filterwarnings('always', category=ImportWarning)
for module_name in PRIVATE_BUT_PRESENT_MODULES:
if not check_importable(module_name):
module_names.append(module_name)
if module_names:
raise AssertionError("Modules that are not really public but looked "
"public and can not be imported: "
f"{module_names}")
@pytest.mark.parametrize(("module_name", "correct_module"),
[('scipy.constants.codata', None),
('scipy.constants.constants', None),
('scipy.fftpack.basic', None),
('scipy.fftpack.helper', None),
('scipy.fftpack.pseudo_diffs', None),
('scipy.fftpack.realtransforms', None),
('scipy.integrate.dop', None),
('scipy.integrate.lsoda', None),
('scipy.integrate.odepack', None),
('scipy.integrate.quadpack', None),
('scipy.integrate.vode', None),
('scipy.interpolate.fitpack', None),
('scipy.interpolate.fitpack2', None),
('scipy.interpolate.interpolate', None),
('scipy.interpolate.ndgriddata', None),
('scipy.interpolate.polyint', None),
('scipy.interpolate.rbf', None),
('scipy.io.harwell_boeing', None),
('scipy.io.idl', None),
('scipy.io.mmio', None),
('scipy.io.netcdf', None),
('scipy.io.arff.arffread', 'arff'),
('scipy.io.matlab.byteordercodes', 'matlab'),
('scipy.io.matlab.mio_utils', 'matlab'),
('scipy.io.matlab.mio', 'matlab'),
('scipy.io.matlab.mio4', 'matlab'),
('scipy.io.matlab.mio5_params', 'matlab'),
('scipy.io.matlab.mio5_utils', 'matlab'),
('scipy.io.matlab.mio5', 'matlab'),
('scipy.io.matlab.miobase', 'matlab'),
('scipy.io.matlab.streams', 'matlab'),
('scipy.linalg.basic', None),
('scipy.linalg.decomp', None),
('scipy.linalg.decomp_cholesky', None),
('scipy.linalg.decomp_lu', None),
('scipy.linalg.decomp_qr', None),
('scipy.linalg.decomp_schur', None),
('scipy.linalg.decomp_svd', None),
('scipy.linalg.matfuncs', None),
('scipy.linalg.misc', None),
('scipy.linalg.special_matrices', None),
('scipy.misc.common', None),
('scipy.ndimage.filters', None),
('scipy.ndimage.fourier', None),
('scipy.ndimage.interpolation', None),
('scipy.ndimage.measurements', None),
('scipy.ndimage.morphology', None),
('scipy.odr.models', None),
('scipy.odr.odrpack', None),
('scipy.optimize.cobyla', None),
('scipy.optimize.lbfgsb', None),
('scipy.optimize.linesearch', None),
('scipy.optimize.minpack', None),
('scipy.optimize.minpack2', None),
('scipy.optimize.moduleTNC', None),
('scipy.optimize.nonlin', None),
('scipy.optimize.optimize', None),
('scipy.optimize.slsqp', None),
('scipy.optimize.tnc', None),
('scipy.optimize.zeros', None),
('scipy.signal.bsplines', None),
('scipy.signal.filter_design', None),
('scipy.signal.fir_filter_design', None),
('scipy.signal.lti_conversion', None),
('scipy.signal.ltisys', None),
('scipy.signal.signaltools', None),
('scipy.signal.spectral', None),
('scipy.signal.waveforms', None),
('scipy.signal.wavelets', None),
('scipy.signal.windows.windows', 'windows'),
('scipy.sparse.lil', None),
('scipy.sparse.linalg.dsolve', 'linalg'),
('scipy.sparse.linalg.eigen', 'linalg'),
('scipy.sparse.linalg.interface', 'linalg'),
('scipy.sparse.linalg.isolve', 'linalg'),
('scipy.sparse.linalg.matfuncs', 'linalg'),
('scipy.sparse.sparsetools', None),
('scipy.sparse.spfuncs', None),
('scipy.sparse.sputils', None),
('scipy.spatial.ckdtree', None),
('scipy.spatial.kdtree', None),
('scipy.spatial.qhull', None),
('scipy.spatial.transform.rotation', 'transform'),
('scipy.special.add_newdocs', None),
('scipy.special.basic', None),
('scipy.special.orthogonal', None),
('scipy.special.sf_error', None),
('scipy.special.specfun', None),
('scipy.special.spfun_stats', None),
('scipy.stats.biasedurn', None),
('scipy.stats.kde', None),
('scipy.stats.morestats', None),
('scipy.stats.mstats_basic', 'mstats'),
('scipy.stats.mstats_extras', 'mstats'),
('scipy.stats.mvn', None),
('scipy.stats.stats', None)])
def test_private_but_present_deprecation(module_name, correct_module):
# gh-18279, gh-17572, gh-17771 noted that deprecation warnings
# for imports from private modules
# were misleading. Check that this is resolved.
module = import_module(module_name)
if correct_module is None:
import_name = f'scipy.{module_name.split(".")[1]}'
else:
import_name = f'scipy.{module_name.split(".")[1]}.{correct_module}'
correct_import = import_module(import_name)
# Attributes that were formerly in `module_name` can still be imported from
# `module_name`, albeit with a deprecation warning. The specific message
# depends on whether the attribute is public in `scipy.xxx` or not.
for attr_name in module.__all__:
attr = getattr(correct_import, attr_name, None)
if attr is None:
message = f"`{module_name}.{attr_name}` is deprecated..."
else:
message = f"Please import `{attr_name}` from the `{import_name}`..."
with pytest.deprecated_call(match=message):
getattr(module, attr_name)
# Attributes that were not in `module_name` get an error notifying the user
# that the attribute is not in `module_name` and that `module_name` is deprecated.
message = f"`{module_name}` is deprecated..."
with pytest.raises(AttributeError, match=message):
getattr(module, "ekki")
def test_misc_doccer_deprecation():
# gh-18279, gh-17572, gh-17771 noted that deprecation warnings
# for imports from private modules were misleading.
# Check that this is resolved.
# `test_private_but_present_deprecation` cannot be used since `correct_import`
# is a different subpackage (`_lib` instead of `misc`).
module = import_module('scipy.misc.doccer')
correct_import = import_module('scipy._lib.doccer')
# Attributes that were formerly in `scipy.misc.doccer` can still be imported from
# `scipy.misc.doccer`, albeit with a deprecation warning. The specific message
# depends on whether the attribute is in `scipy._lib.doccer` or not.
for attr_name in module.__all__:
attr = getattr(correct_import, attr_name, None)
if attr is None:
message = f"`scipy.misc.{attr_name}` is deprecated..."
else:
message = f"Please import `{attr_name}` from the `scipy._lib.doccer`..."
with pytest.deprecated_call(match=message):
getattr(module, attr_name)
# Attributes that were not in `scipy.misc.doccer` get an error
# notifying the user that the attribute is not in `scipy.misc.doccer`
# and that `scipy.misc.doccer` is deprecated.
message = "`scipy.misc.doccer` is deprecated..."
with pytest.raises(AttributeError, match=message):
getattr(module, "ekki")