cmrit
/
cmrithackathon-master
/.venv
/lib
/python3.11
/site-packages
/numpy
/tests
/test_public_api.py
import sys | |
import sysconfig | |
import subprocess | |
import pkgutil | |
import types | |
import importlib | |
import inspect | |
import warnings | |
import numpy as np | |
import numpy | |
from numpy.testing import IS_WASM | |
import pytest | |
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): | |
if name == "core": | |
continue | |
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(): | |
# We override dir to not show these members | |
allowlist = { | |
'recarray': 'numpy.rec.recarray', | |
'show_config': 'numpy.__config__.show', | |
} | |
bad_results = check_dir(np) | |
# pytest gives better error messages with the builtin assert than with | |
# assert_equal | |
assert bad_results == allowlist | |
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 == {} | |
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 [ | |
"ctypeslib", | |
"dtypes", | |
"exceptions", | |
"f2py", | |
"fft", | |
"lib", | |
"lib.array_utils", | |
"lib.format", | |
"lib.introspect", | |
"lib.mixins", | |
"lib.npyio", | |
"lib.recfunctions", # note: still needs cleaning, was forgotten for 2.0 | |
"lib.scimath", | |
"lib.stride_tricks", | |
"linalg", | |
"ma", | |
"ma.extras", | |
"ma.mrecords", | |
"polynomial", | |
"polynomial.chebyshev", | |
"polynomial.hermite", | |
"polynomial.hermite_e", | |
"polynomial.laguerre", | |
"polynomial.legendre", | |
"polynomial.polynomial", | |
"random", | |
"strings", | |
"testing", | |
"testing.overrides", | |
"typing", | |
"typing.mypy_plugin", | |
"version", | |
]] | |
if sys.version_info < (3, 12): | |
PUBLIC_MODULES += [ | |
'numpy.' + s for s in [ | |
"distutils", | |
"distutils.cpuinfo", | |
"distutils.exec_command", | |
"distutils.misc_util", | |
"distutils.log", | |
"distutils.system_info", | |
] | |
] | |
PUBLIC_ALIASED_MODULES = [ | |
"numpy.char", | |
"numpy.emath", | |
"numpy.rec", | |
] | |
PRIVATE_BUT_PRESENT_MODULES = ['numpy.' + s for s in [ | |
"compat", | |
"compat.py3k", | |
"conftest", | |
"core", | |
"core.multiarray", | |
"core.numeric", | |
"core.umath", | |
"core.arrayprint", | |
"core.defchararray", | |
"core.einsumfunc", | |
"core.fromnumeric", | |
"core.function_base", | |
"core.getlimits", | |
"core.numerictypes", | |
"core.overrides", | |
"core.records", | |
"core.shape_base", | |
"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.user_array", # note: not in np.lib, but probably should just be deleted | |
"linalg.lapack_lite", | |
"linalg.linalg", | |
"ma.core", | |
"ma.testutils", | |
"ma.timer_comparison", | |
"matlib", | |
"matrixlib", | |
"matrixlib.defmatrix", | |
"polynomial.polyutils", | |
"random.mtrand", | |
"random.bit_generator", | |
"testing.print_coercion_tables", | |
]] | |
if sys.version_info < (3, 12): | |
PRIVATE_BUT_PRESENT_MODULES += [ | |
'numpy.' + s for s in [ | |
"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", | |
] | |
] | |
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 | |
if sys.version_info < (3, 12): | |
SKIP_LIST = ["numpy.distutils.msvc9compiler"] | |
else: | |
SKIP_LIST = [] | |
# suppressing warnings from deprecated modules | |
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.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', | |
] | |
if sys.version_info < (3, 12): | |
SKIP_LIST_2 += [ | |
'numpy.distutils.log.sys', | |
'numpy.distutils.log.logging', | |
'numpy.distutils.log.warnings', | |
] | |
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)) | |
def test_array_api_entry_point(): | |
""" | |
Entry point for Array API implementation can be found with importlib and | |
returns the main numpy namespace. | |
""" | |
# For a development install that did not go through meson-python, | |
# the entrypoint will not have been installed. So ensure this test fails | |
# only if numpy is inside site-packages. | |
numpy_in_sitepackages = sysconfig.get_path('platlib') in np.__file__ | |
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", []) | |
if len(xp_eps) == 0: | |
if numpy_in_sitepackages: | |
msg = "No entry points for 'array_api' found" | |
raise AssertionError(msg) from None | |
return | |
try: | |
ep = next(ep for ep in xp_eps if ep.name == "numpy") | |
except StopIteration: | |
if numpy_in_sitepackages: | |
msg = "'numpy' not in array_api entry points" | |
raise AssertionError(msg) from None | |
return | |
if ep.value == 'numpy.array_api': | |
# Looks like the entrypoint for the current numpy build isn't | |
# installed, but an older numpy is also installed and hence the | |
# entrypoint is pointing to the old (no longer existing) location. | |
# This isn't a problem except for when running tests with `spin` or an | |
# in-place build. | |
return | |
xp = ep.load() | |
msg = ( | |
f"numpy entry point value '{ep.value}' " | |
"does not point to our Array API implementation" | |
) | |
assert xp is numpy, msg | |
def test_main_namespace_all_dir_coherence(): | |
""" | |
Checks if `dir(np)` and `np.__all__` are consistent and return | |
the same content, excluding exceptions and private members. | |
""" | |
def _remove_private_members(member_set): | |
return {m for m in member_set if not m.startswith('_')} | |
def _remove_exceptions(member_set): | |
return member_set.difference({ | |
"bool" # included only in __dir__ | |
}) | |
all_members = _remove_private_members(np.__all__) | |
all_members = _remove_exceptions(all_members) | |
dir_members = _remove_private_members(np.__dir__()) | |
dir_members = _remove_exceptions(dir_members) | |
assert all_members == dir_members, ( | |
"Members that break symmetry: " | |
f"{all_members.symmetric_difference(dir_members)}" | |
) | |
def test_core_shims_coherence(): | |
""" | |
Check that all "semi-public" members of `numpy._core` are also accessible | |
from `numpy.core` shims. | |
""" | |
import numpy.core as core | |
for member_name in dir(np._core): | |
# Skip private and test members. Also if a module is aliased, | |
# no need to add it to np.core | |
if ( | |
member_name.startswith("_") | |
or member_name in ["tests", "strings"] | |
or f"numpy.{member_name}" in PUBLIC_ALIASED_MODULES | |
): | |
continue | |
member = getattr(np._core, member_name) | |
# np.core is a shim and all submodules of np.core are shims | |
# but we should be able to import everything in those shims | |
# that are available in the "real" modules in np._core | |
if inspect.ismodule(member): | |
submodule = member | |
submodule_name = member_name | |
for submodule_member_name in dir(submodule): | |
# ignore dunder names | |
if submodule_member_name.startswith("__"): | |
continue | |
submodule_member = getattr(submodule, submodule_member_name) | |
core_submodule = __import__( | |
f"numpy.core.{submodule_name}", | |
fromlist=[submodule_member_name] | |
) | |
assert submodule_member is getattr( | |
core_submodule, submodule_member_name | |
) | |
else: | |
assert member is getattr(core, member_name) | |
def test_functions_single_location(): | |
""" | |
Check that each public function is available from one location only. | |
Test performs BFS search traversing NumPy's public API. It flags | |
any function-like object that is accessible from more that one place. | |
""" | |
from typing import Any, Callable, Dict, List, Set, Tuple | |
from numpy._core._multiarray_umath import ( | |
_ArrayFunctionDispatcher as dispatched_function | |
) | |
visited_modules: Set[types.ModuleType] = {np} | |
visited_functions: Set[Callable[..., Any]] = set() | |
# Functions often have `__name__` overridden, therefore we need | |
# to keep track of locations where functions have been found. | |
functions_original_paths: Dict[Callable[..., Any], str] = dict() | |
# Here we aggregate functions with more than one location. | |
# It must be empty for the test to pass. | |
duplicated_functions: List[Tuple] = [] | |
modules_queue = [np] | |
while len(modules_queue) > 0: | |
module = modules_queue.pop() | |
for member_name in dir(module): | |
member = getattr(module, member_name) | |
# first check if we got a module | |
if ( | |
inspect.ismodule(member) and # it's a module | |
"numpy" in member.__name__ and # inside NumPy | |
not member_name.startswith("_") and # not private | |
"numpy._core" not in member.__name__ and # outside _core | |
# not a legacy or testing module | |
member_name not in ["f2py", "ma", "testing", "tests"] and | |
member not in visited_modules # not visited yet | |
): | |
modules_queue.append(member) | |
visited_modules.add(member) | |
# else check if we got a function-like object | |
elif ( | |
inspect.isfunction(member) or | |
isinstance(member, (dispatched_function, np.ufunc)) | |
): | |
if member in visited_functions: | |
# skip main namespace functions with aliases | |
if ( | |
member.__name__ in [ | |
"absolute", # np.abs | |
"arccos", # np.acos | |
"arccosh", # np.acosh | |
"arcsin", # np.asin | |
"arcsinh", # np.asinh | |
"arctan", # np.atan | |
"arctan2", # np.atan2 | |
"arctanh", # np.atanh | |
"left_shift", # np.bitwise_left_shift | |
"right_shift", # np.bitwise_right_shift | |
"conjugate", # np.conj | |
"invert", # np.bitwise_not & np.bitwise_invert | |
"remainder", # np.mod | |
"divide", # np.true_divide | |
"concatenate", # np.concat | |
"power", # np.pow | |
"transpose", # np.permute_dims | |
] and | |
module.__name__ == "numpy" | |
): | |
continue | |
# skip trimcoef from numpy.polynomial as it is | |
# duplicated by design. | |
if ( | |
member.__name__ == "trimcoef" and | |
module.__name__.startswith("numpy.polynomial") | |
): | |
continue | |
# skip ufuncs that are exported in np.strings as well | |
if member.__name__ in ( | |
"add", | |
"equal", | |
"not_equal", | |
"greater", | |
"greater_equal", | |
"less", | |
"less_equal", | |
) and module.__name__ == "numpy.strings": | |
continue | |
# numpy.char reexports all numpy.strings functions for | |
# backwards-compatibility | |
if module.__name__ == "numpy.char": | |
continue | |
# function is present in more than one location! | |
duplicated_functions.append( | |
(member.__name__, | |
module.__name__, | |
functions_original_paths[member]) | |
) | |
else: | |
visited_functions.add(member) | |
functions_original_paths[member] = module.__name__ | |
del visited_functions, visited_modules, functions_original_paths | |
assert len(duplicated_functions) == 0, duplicated_functions | |