327 lines
9.7 KiB
Python
327 lines
9.7 KiB
Python
"""
|
|
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
|
|
|
|
import scipy
|
|
|
|
|
|
def check_dir(module, module_name=None):
|
|
"""Returns a mapping of all objects with the wrong __module__ attribute."""
|
|
if module_name is None:
|
|
module_name = module.__name__
|
|
results = {}
|
|
for name in dir(module):
|
|
item = getattr(module, name)
|
|
if (hasattr(item, '__module__') and hasattr(item, '__name__')
|
|
and item.__module__ != module_name):
|
|
results[name] = item.__module__ + '.' + item.__name__
|
|
return results
|
|
|
|
|
|
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 look public (lack
|
|
# of underscores) but should not be used. For many of those modules the
|
|
# current status is fine. For others it may make sense to work on making them
|
|
# private, to clean up our public API and avoid confusion.
|
|
# These private modules support will be removed in SciPy v2.0.0
|
|
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.odepack',
|
|
'scipy.integrate.quadpack',
|
|
'scipy.integrate.dop',
|
|
'scipy.integrate.lsoda',
|
|
'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.mmio',
|
|
'scipy.io.netcdf',
|
|
'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.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.flinalg',
|
|
'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.statlib',
|
|
'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',
|
|
]
|
|
|
|
|
|
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.
|
|
"""
|
|
|
|
modnames = []
|
|
for _, modname, ispkg in pkgutil.walk_packages(path=scipy.__path__,
|
|
prefix=scipy.__name__ + '.',
|
|
onerror=None):
|
|
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 "
|
|
"modules: {}".format(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 "
|
|
"imported: {}".format(module_names))
|
|
|
|
with warnings.catch_warnings(record=True) as w:
|
|
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: "
|
|
"{}".format(module_names))
|