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
@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 [
"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
@pytest.mark.filterwarnings("ignore:.*np.compat.*:DeprecationWarning")
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))
@pytest.mark.xfail(
sysconfig.get_config_var("Py_DEBUG") not in (None, 0, "0"),
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 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)}"
)
@pytest.mark.filterwarnings(
r"ignore:numpy.core(\.\w+)? is deprecated:DeprecationWarning"
)
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