123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326 |
- """
- 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))
|