| Viewing file:  test_public_api.py (16.48 KB)      -rw-r--r-- Select action/file-type:
 
  (+) |  (+) |  (+) | Code (+) | Session (+) |  (+) | SDB (+) |  (+) |  (+) |  (+) |  (+) |  (+) | 
 
import sysimport 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 = {
 '_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'])
 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",
 "dtypes",
 "exceptions",
 "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",
 "testing.overrides",
 "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.fujitsuccompiler",
 "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",
 "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.core",
 "ma.testutils",
 "ma.timer_comparison",
 "matrixlib",
 "matrixlib.defmatrix",
 "polynomial.polyutils",
 "random.mtrand",
 "random.bit_generator",
 "testing.print_coercion_tables",
 ]]
 
 
 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.code_generators.verify_c_api_version",
 "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.exceptions',
 '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(
 hasattr(np.__config__, "_built_with_meson"),
 reason = "Meson does not yet support entry points via pyproject.toml",
 )
 @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
 
 
 @pytest.mark.parametrize("name", [
 'ModuleDeprecationWarning', 'VisibleDeprecationWarning',
 'ComplexWarning', 'TooHardError', 'AxisError'])
 def test_moved_exceptions(name):
 # These were moved to the exceptions namespace, but currently still
 # available
 assert name in np.__all__
 assert name not in np.__dir__()
 # Fetching works, but __module__ is set correctly:
 assert getattr(np, name).__module__ == "numpy.exceptions"
 assert name in np.exceptions.__all__
 getattr(np.exceptions, name)
 
 |