123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506 |
- import sys
- import sysconfig
- import subprocess
- import pkgutil
- import types
- import importlib
- import warnings
- import numpy as np
- import numpy
- import pytest
- from numpy.testing import IS_WASM
- try:
- import ctypes
- except ImportError:
- ctypes = None
- 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_numpy_namespace():
- # None of these objects are publicly documented to be part of the main
- # NumPy namespace (some are useful though, others need to be cleaned up)
- undocumented = {
- 'Tester': 'numpy.testing._private.nosetester.NoseTester',
- '_add_newdoc_ufunc': 'numpy.core._multiarray_umath._add_newdoc_ufunc',
- 'add_docstring': 'numpy.core._multiarray_umath.add_docstring',
- 'add_newdoc': 'numpy.core.function_base.add_newdoc',
- 'add_newdoc_ufunc': 'numpy.core._multiarray_umath._add_newdoc_ufunc',
- 'byte_bounds': 'numpy.lib.utils.byte_bounds',
- 'compare_chararrays': 'numpy.core._multiarray_umath.compare_chararrays',
- 'deprecate': 'numpy.lib.utils.deprecate',
- 'deprecate_with_doc': 'numpy.lib.utils.deprecate_with_doc',
- 'disp': 'numpy.lib.function_base.disp',
- 'fastCopyAndTranspose': 'numpy.core._multiarray_umath.fastCopyAndTranspose',
- 'get_array_wrap': 'numpy.lib.shape_base.get_array_wrap',
- 'get_include': 'numpy.lib.utils.get_include',
- 'recfromcsv': 'numpy.lib.npyio.recfromcsv',
- 'recfromtxt': 'numpy.lib.npyio.recfromtxt',
- 'safe_eval': 'numpy.lib.utils.safe_eval',
- 'set_string_function': 'numpy.core.arrayprint.set_string_function',
- 'show_config': 'numpy.__config__.show',
- 'show_runtime': 'numpy.lib.utils.show_runtime',
- 'who': 'numpy.lib.utils.who',
- }
- # We override dir to not show these members
- allowlist = undocumented
- bad_results = check_dir(np)
- # pytest gives better error messages with the builtin assert than with
- # assert_equal
- assert bad_results == allowlist
- @pytest.mark.skipif(IS_WASM, reason="can't start subprocess")
- @pytest.mark.parametrize('name', ['testing', 'Tester'])
- def test_import_lazy_import(name):
- """Make sure we can actually use the modules we lazy load.
- While not exported as part of the public API, it was accessible. With the
- use of __getattr__ and __dir__, this isn't always true It can happen that
- an infinite recursion may happen.
- This is the only way I found that would force the failure to appear on the
- badly implemented code.
- We also test for the presence of the lazily imported modules in dir
- """
- exe = (sys.executable, '-c', "import numpy; numpy." + name)
- result = subprocess.check_output(exe)
- assert not result
- # Make sure they are still in the __dir__
- assert name in dir(np)
- def test_dir_testing():
- """Assert that output of dir has only one "testing/tester"
- attribute without duplicate"""
- assert len(dir(np)) == len(set(dir(np)))
- def test_numpy_linalg():
- bad_results = check_dir(np.linalg)
- assert bad_results == {}
- def test_numpy_fft():
- bad_results = check_dir(np.fft)
- assert bad_results == {}
- @pytest.mark.skipif(ctypes is None,
- reason="ctypes not available in this python")
- def test_NPY_NO_EXPORT():
- cdll = ctypes.CDLL(np.core._multiarray_tests.__file__)
- # Make sure an arbitrary NPY_NO_EXPORT function is actually hidden
- f = getattr(cdll, 'test_not_exported', None)
- assert f is None, ("'test_not_exported' is mistakenly exported, "
- "NPY_NO_EXPORT does not work")
- # Historically NumPy 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 numpy.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.
- #
- # 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.
- PUBLIC_MODULES = ['numpy.' + s for s in [
- "array_api",
- "array_api.linalg",
- "ctypeslib",
- "distutils",
- "distutils.cpuinfo",
- "distutils.exec_command",
- "distutils.misc_util",
- "distutils.log",
- "distutils.system_info",
- "doc",
- "doc.constants",
- "doc.ufuncs",
- "f2py",
- "fft",
- "lib",
- "lib.format", # was this meant to be public?
- "lib.mixins",
- "lib.recfunctions",
- "lib.scimath",
- "lib.stride_tricks",
- "linalg",
- "ma",
- "ma.extras",
- "ma.mrecords",
- "matlib",
- "polynomial",
- "polynomial.chebyshev",
- "polynomial.hermite",
- "polynomial.hermite_e",
- "polynomial.laguerre",
- "polynomial.legendre",
- "polynomial.polynomial",
- "random",
- "testing",
- "typing",
- "typing.mypy_plugin",
- "version",
- ]]
- PUBLIC_ALIASED_MODULES = [
- "numpy.char",
- "numpy.emath",
- "numpy.rec",
- ]
- PRIVATE_BUT_PRESENT_MODULES = ['numpy.' + s for s in [
- "compat",
- "compat.py3k",
- "conftest",
- "core",
- "core.arrayprint",
- "core.defchararray",
- "core.einsumfunc",
- "core.fromnumeric",
- "core.function_base",
- "core.getlimits",
- "core.memmap",
- "core.multiarray",
- "core.numeric",
- "core.numerictypes",
- "core.overrides",
- "core.records",
- "core.shape_base",
- "core.umath",
- "core.umath_tests",
- "distutils.armccompiler",
- "distutils.ccompiler",
- 'distutils.ccompiler_opt',
- "distutils.command",
- "distutils.command.autodist",
- "distutils.command.bdist_rpm",
- "distutils.command.build",
- "distutils.command.build_clib",
- "distutils.command.build_ext",
- "distutils.command.build_py",
- "distutils.command.build_scripts",
- "distutils.command.build_src",
- "distutils.command.config",
- "distutils.command.config_compiler",
- "distutils.command.develop",
- "distutils.command.egg_info",
- "distutils.command.install",
- "distutils.command.install_clib",
- "distutils.command.install_data",
- "distutils.command.install_headers",
- "distutils.command.sdist",
- "distutils.conv_template",
- "distutils.core",
- "distutils.extension",
- "distutils.fcompiler",
- "distutils.fcompiler.absoft",
- "distutils.fcompiler.arm",
- "distutils.fcompiler.compaq",
- "distutils.fcompiler.environment",
- "distutils.fcompiler.g95",
- "distutils.fcompiler.gnu",
- "distutils.fcompiler.hpux",
- "distutils.fcompiler.ibm",
- "distutils.fcompiler.intel",
- "distutils.fcompiler.lahey",
- "distutils.fcompiler.mips",
- "distutils.fcompiler.nag",
- "distutils.fcompiler.none",
- "distutils.fcompiler.pathf95",
- "distutils.fcompiler.pg",
- "distutils.fcompiler.nv",
- "distutils.fcompiler.sun",
- "distutils.fcompiler.vast",
- "distutils.fcompiler.fujitsu",
- "distutils.from_template",
- "distutils.intelccompiler",
- "distutils.lib2def",
- "distutils.line_endings",
- "distutils.mingw32ccompiler",
- "distutils.msvccompiler",
- "distutils.npy_pkg_config",
- "distutils.numpy_distribution",
- "distutils.pathccompiler",
- "distutils.unixccompiler",
- "dual",
- "f2py.auxfuncs",
- "f2py.capi_maps",
- "f2py.cb_rules",
- "f2py.cfuncs",
- "f2py.common_rules",
- "f2py.crackfortran",
- "f2py.diagnose",
- "f2py.f2py2e",
- "f2py.f90mod_rules",
- "f2py.func2subr",
- "f2py.rules",
- "f2py.symbolic",
- "f2py.use_rules",
- "fft.helper",
- "lib.arraypad",
- "lib.arraysetops",
- "lib.arrayterator",
- "lib.function_base",
- "lib.histograms",
- "lib.index_tricks",
- "lib.nanfunctions",
- "lib.npyio",
- "lib.polynomial",
- "lib.shape_base",
- "lib.twodim_base",
- "lib.type_check",
- "lib.ufunclike",
- "lib.user_array", # note: not in np.lib, but probably should just be deleted
- "lib.utils",
- "linalg.lapack_lite",
- "linalg.linalg",
- "ma.bench",
- "ma.core",
- "ma.testutils",
- "ma.timer_comparison",
- "matrixlib",
- "matrixlib.defmatrix",
- "polynomial.polyutils",
- "random.mtrand",
- "random.bit_generator",
- "testing.print_coercion_tables",
- "testing.utils",
- ]]
- 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 PUBLIC_ALIASED_MODULES:
- return False
- if name in PRIVATE_BUT_PRESENT_MODULES:
- return False
- return True
- # These are present in a directory with an __init__.py but cannot be imported
- # code_generators/ isn't installed, but present for an inplace build
- SKIP_LIST = [
- "numpy.core.code_generators",
- "numpy.core.code_generators.genapi",
- "numpy.core.code_generators.generate_umath",
- "numpy.core.code_generators.ufunc_docstrings",
- "numpy.core.code_generators.generate_numpy_api",
- "numpy.core.code_generators.generate_ufunc_api",
- "numpy.core.code_generators.numpy_api",
- "numpy.core.code_generators.generate_umath_doc",
- "numpy.core.cversions",
- "numpy.core.generate_numpy_api",
- "numpy.distutils.msvc9compiler",
- ]
- 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=np.__path__,
- prefix=np.__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 = [
- 'numpy.math',
- 'numpy.distutils.log.sys',
- 'numpy.distutils.log.logging',
- 'numpy.distutils.log.warnings',
- 'numpy.doc.constants.re',
- 'numpy.doc.constants.textwrap',
- 'numpy.lib.emath',
- 'numpy.lib.math',
- 'numpy.matlib.char',
- 'numpy.matlib.rec',
- 'numpy.matlib.emath',
- 'numpy.matlib.math',
- 'numpy.matlib.linalg',
- 'numpy.matlib.fft',
- 'numpy.matlib.random',
- 'numpy.matlib.ctypeslib',
- 'numpy.matlib.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. So this test is more thorough, and checks this like:
- import .lib.scimath as emath
- To check if something in a module is (effectively) public, one can check if
- there's anything in that namespace that's a public function/object but is
- not exposed in a higher-level namespace. For example for a `numpy.lib`
- submodule::
- mod = np.lib.mixins
- for obj in mod.__all__:
- if obj in np.__all__:
- continue
- elif obj in np.lib.__all__:
- continue
- else:
- print(obj)
- """
- 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):
- if fullobjname not in SKIP_LIST_2:
- members.append(fullobjname)
- return members
- unexpected_members = find_unexpected_members("numpy")
- 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))
- for module_name in PUBLIC_ALIASED_MODULES:
- try:
- eval(module_name)
- except AttributeError:
- module_names.append(module_name)
- if module_names:
- raise AssertionError("Modules in the public API that were not "
- "found: {}".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))
- @pytest.mark.xfail(
- sysconfig.get_config_var("Py_DEBUG") is not None,
- reason=(
- "NumPy possibly built with `USE_DEBUG=True ./tools/travis-test.sh`, "
- "which does not expose the `array_api` entry point. "
- "See https://github.com/numpy/numpy/pull/19800"
- ),
- )
- def test_array_api_entry_point():
- """
- Entry point for Array API implementation can be found with importlib and
- returns the numpy.array_api namespace.
- """
- eps = importlib.metadata.entry_points()
- try:
- xp_eps = eps.select(group="array_api")
- except AttributeError:
- # The select interface for entry_points was introduced in py3.10,
- # deprecating its dict interface. We fallback to dict keys for finding
- # Array API entry points so that running this test in <=3.9 will
- # still work - see https://github.com/numpy/numpy/pull/19800.
- xp_eps = eps.get("array_api", [])
- assert len(xp_eps) > 0, "No entry points for 'array_api' found"
- try:
- ep = next(ep for ep in xp_eps if ep.name == "numpy")
- except StopIteration:
- raise AssertionError("'numpy' not in array_api entry points") from None
- xp = ep.load()
- msg = (
- f"numpy entry point value '{ep.value}' "
- "does not point to our Array API implementation"
- )
- assert xp is numpy.array_api, msg
|