From 8753d296c9dbdca9e58bcdc9028c794c7271e005 Mon Sep 17 00:00:00 2001 From: Ralf Gommers Date: Sun, 8 Sep 2019 00:54:29 -0700 Subject: [PATCH 01/10] TST: add test to prevent new public-looking modules being added --- numpy/tests/test_public_api.py | 289 ++++++++++++++++++++++++++++++++- 1 file changed, 288 insertions(+), 1 deletion(-) diff --git a/numpy/tests/test_public_api.py b/numpy/tests/test_public_api.py index df2fc4802a58..5c5de7c234ac 100644 --- a/numpy/tests/test_public_api.py +++ b/numpy/tests/test_public_api.py @@ -2,14 +2,20 @@ import sys import subprocess +import pkgutil +import types +import importlib import numpy as np +import numpy 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: @@ -72,7 +78,7 @@ def test_numpy_namespace(): @pytest.mark.parametrize('name', ['testing', 'Tester']) def test_import_lazy_import(name): - """Make sure we can actually the the modules we lazy load. + """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 @@ -101,6 +107,7 @@ 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(): @@ -109,3 +116,283 @@ def test_NPY_NO_EXPORT(): f = getattr(cdll, 'test_not_exported', None) assert f is None, ("'test_not_exported' is mistakenly exported, " "NPY_NO_EXPORT does not work") + + +PUBLIC_MODULES = [ + "ctypeslib", + "distutils", + "distutils.cpuinfo", + "distutils.exec_command", + "distutils.misc_util", + "distutils.log", + "distutils.system_info", + "doc", + "doc.basics", + "doc.broadcasting", + "doc.byteswapping", + "doc.constants", + "doc.creation", + "doc.dispatch", + "doc.glossary", + "doc.indexing", + "doc.internals", + "doc.misc", + "doc.structured_arrays", + "doc.subclassing", + "doc.ufuncs", + "dual", + "f2py", + "fft", + "lib", + "lib.format", + "lib.mixins", + "lib.npyio", + "lib.recfunctions", + "lib.scimath", + "linalg", + "ma", + "ma.extras", + "ma.mrecords", + "matlib", + "polynomial", + "polynomial.chebyshev", + "polynomial.hermite", + "polynomial.hermite_e", + "polynomial.laguerre", + "polynomial.legendre", + "polynomial.polynomial", + "polynomial.polyutils", + "random", + "testing", + "version", +] + + +PUBLIC_ALIASED_MODULES = [ + "char", + "emath", + "rec", +] + + +PRIVATE_BUT_PRESENT_MODULES = [ + "compat", + "compat.py3k", + "conftest", + "core", + "core.arrayprint", + "core.code_generators", + "core.code_generators.genapi", + "core.code_generators.generate_numpy_api", + "core.code_generators.generate_ufunc_api", + "core.code_generators.generate_umath", + "core.code_generators.numpy_api", + "core.code_generators.ufunc_docstrings", + "core.cversions", + "core.defchararray", + "core.einsumfunc", + "core.fromnumeric", + "core.function_base", + "core.getlimits", + "core.info", + "core.machar", + "core.memmap", + "core.multiarray", + "core.numeric", + "core.numerictypes", + "core.overrides", + "core.records", + "core.shape_base", + "core.umath", + "core.umath_tests", + "distutils.ccompiler", + "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.compat", + "distutils.conv_template", + "distutils.core", + "distutils.extension", + "distutils.fcompiler", + "distutils.fcompiler.absoft", + "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.sun", + "distutils.fcompiler.vast", + "distutils.from_template", + "distutils.info", + "distutils.intelccompiler", + "distutils.lib2def", + "distutils.line_endings", + "distutils.mingw32ccompiler", + "distutils.msvc9compiler", + "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.f2py_testing", + "f2py.f90mod_rules", + "f2py.func2subr", + "f2py.info", + "f2py.rules", + "f2py.use_rules", + "fft.helper", + "fft.info", + "fft.pocketfft", + "fft.pocketfft_internal", + "lib.arraypad", # TODO: figure out which numpy.lib submodules are public + "lib.arraysetops", + "lib.arrayterator", + "lib.financial", + "lib.function_base", + "lib.histograms", + "lib.index_tricks", + "lib.info", + "lib.nanfunctions", + "lib.polynomial", + "lib.shape_base", + "lib.stride_tricks", + "lib.twodim_base", + "lib.type_check", + "lib.ufunclike", + "lib.user_array", + "lib.utils", + "linalg.info", + "linalg.lapack_lite", + "linalg.linalg", + "ma.bench", + "ma.core", + "ma.testutils", + "ma.timer_comparison", + "ma.version", + "matrixlib", + "matrixlib.defmatrix", + "random.bit_generator", + "random.bounded_integers", + "random.common", + "random.entropy", + "random.generator", + "random.info", + "random.mt19937", + "random.mtrand", + "random.pcg64", + "random.philox", + "random.sfc64", + "testing.decorators", + "testing.noseclasses", + "testing.nosetester", + "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.startswith("numpy."): + name = name[6:] + + 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 + + +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): + # 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("Found unexpected modules: {}".format(modnames)) + + +@pytest.mark.xfail(reason="missing __all__ dicts are messing this up, " + "needs work") +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 + + """ + modnames = [] + + def check(modname): + module = importlib.import_module(modname) + if hasattr(module, '__all__'): + objnames = module.__all__ + else: + objnames = dir(module) + + for objname in objnames: + if not objname.startswith('_'): + fullobjname = modname + '.' + objname + if isinstance(eval(fullobjname), types.ModuleType): + if is_unexpected(fullobjname): + modnames.append(fullobjname) + + check("numpy") + for modname in PUBLIC_MODULES: + check("numpy." + modname) + + if modnames: + raise AssertionError("Found unexpected object(s) that look like " + "modules: {}".format(modnames)) From 53f130751ac488b879adcb9e6bb2e2bc76a4ac93 Mon Sep 17 00:00:00 2001 From: Ralf Gommers Date: Sun, 8 Sep 2019 01:25:42 -0700 Subject: [PATCH 02/10] TST: add `core/generate_numpy_api.py` to private-but-present modules --- numpy/tests/test_public_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/numpy/tests/test_public_api.py b/numpy/tests/test_public_api.py index 5c5de7c234ac..32e9f42d51cd 100644 --- a/numpy/tests/test_public_api.py +++ b/numpy/tests/test_public_api.py @@ -193,6 +193,7 @@ def test_NPY_NO_EXPORT(): "core.einsumfunc", "core.fromnumeric", "core.function_base", + "core.generate_numpy_api", "core.getlimits", "core.info", "core.machar", From 9996fb316d7386a0ea3b5f85e71283b3ba9abe55 Mon Sep 17 00:00:00 2001 From: Ralf Gommers Date: Sun, 8 Sep 2019 11:23:10 -0700 Subject: [PATCH 03/10] MAINT: fix issue with `np.lib.mixins.__all__` and import of mixins --- numpy/lib/__init__.py | 4 +--- numpy/lib/mixins.py | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/numpy/lib/__init__.py b/numpy/lib/__init__.py index c1757150e597..eb6c9cace184 100644 --- a/numpy/lib/__init__.py +++ b/numpy/lib/__init__.py @@ -8,7 +8,7 @@ from .type_check import * from .index_tricks import * from .function_base import * -from .mixins import * +from . import mixins # public from .nanfunctions import * from .shape_base import * from .stride_tricks import * @@ -18,7 +18,6 @@ from . import scimath as emath from .polynomial import * -#import convertcode from .utils import * from .arraysetops import * from .npyio import * @@ -32,7 +31,6 @@ __all__ += type_check.__all__ __all__ += index_tricks.__all__ __all__ += function_base.__all__ -__all__ += mixins.__all__ __all__ += shape_base.__all__ __all__ += stride_tricks.__all__ __all__ += twodim_base.__all__ diff --git a/numpy/lib/mixins.py b/numpy/lib/mixins.py index 52ad45b68169..f974a7724e4d 100644 --- a/numpy/lib/mixins.py +++ b/numpy/lib/mixins.py @@ -5,8 +5,8 @@ from numpy.core import umath as um -# Nothing should be exposed in the top-level NumPy module. -__all__ = [] + +__all__ = ['NDArrayOperatorsMixin'] def _disables_array_ufunc(obj): From 50cbd096099d71250fa45b2d3332b0faa1341625 Mon Sep 17 00:00:00 2001 From: Ralf Gommers Date: Sun, 8 Sep 2019 11:25:57 -0700 Subject: [PATCH 04/10] TST: add details to the public modules test about how to check public-ness --- numpy/tests/test_public_api.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/numpy/tests/test_public_api.py b/numpy/tests/test_public_api.py index 32e9f42d51cd..4d9066fc9fbd 100644 --- a/numpy/tests/test_public_api.py +++ b/numpy/tests/test_public_api.py @@ -373,6 +373,21 @@ def test_all_modules_are_expected_2(): 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) + """ modnames = [] From d072f12354d1802aa6ac7a3bfa92a24fa5df613d Mon Sep 17 00:00:00 2001 From: Ralf Gommers Date: Sun, 8 Sep 2019 11:57:31 -0700 Subject: [PATCH 05/10] MAINT: add missing 'Arrayterator' to `numpy.lib.__all__` Also finish the TODO about figuring out which np.lib.'s are public. This is a giant mess ... --- numpy/__init__.py | 6 ++++++ numpy/lib/__init__.py | 10 +++++++--- numpy/tests/test_public_api.py | 11 ++++++----- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/numpy/__init__.py b/numpy/__init__.py index 07d67945c681..c5ef6869f42c 100644 --- a/numpy/__init__.py +++ b/numpy/__init__.py @@ -143,7 +143,9 @@ from .core import * from . import compat from . import lib + # FIXME: why have numpy.lib if everything is imported here?? from .lib import * + from . import linalg from . import fft from . import polynomial @@ -174,6 +176,10 @@ __all__.extend(lib.__all__) __all__.extend(['linalg', 'fft', 'random', 'ctypeslib', 'ma']) + # Remove things that are in the numpy.lib but not in the numpy namespace + __all__.remove('Arrayterator') + del Arrayterator + # Filter out Cython harmless warnings warnings.filterwarnings("ignore", message="numpy.dtype size changed") warnings.filterwarnings("ignore", message="numpy.ufunc size changed") diff --git a/numpy/lib/__init__.py b/numpy/lib/__init__.py index eb6c9cace184..906bede37788 100644 --- a/numpy/lib/__init__.py +++ b/numpy/lib/__init__.py @@ -5,10 +5,15 @@ from .info import __doc__ from numpy.version import version as __version__ +# Public submodules +# Note: recfunctions and (maybe) format are public too, but not imported +from . import mixins +from . import scimath as emath + +# Private submodules from .type_check import * from .index_tricks import * from .function_base import * -from . import mixins # public from .nanfunctions import * from .shape_base import * from .stride_tricks import * @@ -16,7 +21,6 @@ from .ufunclike import * from .histograms import * -from . import scimath as emath from .polynomial import * from .utils import * from .arraysetops import * @@ -27,7 +31,7 @@ from ._version import * from numpy.core._multiarray_umath import tracemalloc_domain -__all__ = ['emath', 'math', 'tracemalloc_domain'] +__all__ = ['emath', 'math', 'tracemalloc_domain', 'Arrayterator'] __all__ += type_check.__all__ __all__ += index_tricks.__all__ __all__ += function_base.__all__ diff --git a/numpy/tests/test_public_api.py b/numpy/tests/test_public_api.py index 4d9066fc9fbd..2b29ea0e9232 100644 --- a/numpy/tests/test_public_api.py +++ b/numpy/tests/test_public_api.py @@ -33,7 +33,8 @@ def check_dir(module, module_name=None): sys.version_info[0] < 3, reason="NumPy exposes slightly different functions on Python 2") def test_numpy_namespace(): - # None of these objects are publicly documented. + # 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', @@ -144,9 +145,8 @@ def test_NPY_NO_EXPORT(): "f2py", "fft", "lib", - "lib.format", + "lib.format", # was this meant to be public? "lib.mixins", - "lib.npyio", "lib.recfunctions", "lib.scimath", "linalg", @@ -276,7 +276,7 @@ def test_NPY_NO_EXPORT(): "fft.info", "fft.pocketfft", "fft.pocketfft_internal", - "lib.arraypad", # TODO: figure out which numpy.lib submodules are public + "lib.arraypad", "lib.arraysetops", "lib.arrayterator", "lib.financial", @@ -285,13 +285,14 @@ def test_NPY_NO_EXPORT(): "lib.index_tricks", "lib.info", "lib.nanfunctions", + "lib.npyio", "lib.polynomial", "lib.shape_base", "lib.stride_tricks", "lib.twodim_base", "lib.type_check", "lib.ufunclike", - "lib.user_array", + "lib.user_array", # note: not in np.lib, but probably should just be deleted "lib.utils", "linalg.info", "linalg.lapack_lite", From 8ce48a392600c70adc66647a8c365692eb490da9 Mon Sep 17 00:00:00 2001 From: Ralf Gommers Date: Mon, 9 Sep 2019 16:11:31 -0700 Subject: [PATCH 06/10] DOC: add some comments to explain namespace cleanup in numpy/__init__.py [ci skip] --- numpy/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/numpy/__init__.py b/numpy/__init__.py index c5ef6869f42c..fef8245de3e7 100644 --- a/numpy/__init__.py +++ b/numpy/__init__.py @@ -177,6 +177,10 @@ __all__.extend(['linalg', 'fft', 'random', 'ctypeslib', 'ma']) # Remove things that are in the numpy.lib but not in the numpy namespace + # Note that there is a test (numpy/tests/test_public_api.py:test_numpy_namespace) + # that prevents adding more things to the main namespace by accident. + # The list below will grow until the `from .lib import *` fixme above is + # taken care of __all__.remove('Arrayterator') del Arrayterator From acf62f6881afb2f6e0efc9db6acc44c3c8614528 Mon Sep 17 00:00:00 2001 From: Ralf Gommers Date: Wed, 11 Sep 2019 20:05:57 -0700 Subject: [PATCH 07/10] TST: add comment on public/semi-private modules. Also address other review comments. --- numpy/tests/test_public_api.py | 52 ++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/numpy/tests/test_public_api.py b/numpy/tests/test_public_api.py index 2b29ea0e9232..b6d8a4715b2f 100644 --- a/numpy/tests/test_public_api.py +++ b/numpy/tests/test_public_api.py @@ -119,7 +119,19 @@ def test_NPY_NO_EXPORT(): "NPY_NO_EXPORT does not work") -PUBLIC_MODULES = [ +# 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", "distutils", "distutils.cpuinfo", @@ -165,17 +177,17 @@ def test_NPY_NO_EXPORT(): "random", "testing", "version", -] +]] PUBLIC_ALIASED_MODULES = [ - "char", - "emath", - "rec", + "numpy.char", + "numpy.emath", + "numpy.rec", ] -PRIVATE_BUT_PRESENT_MODULES = [ +PRIVATE_BUT_PRESENT_MODULES = ['numpy.' + s for s in [ "compat", "compat.py3k", "conftest", @@ -320,7 +332,7 @@ def test_NPY_NO_EXPORT(): "testing.nosetester", "testing.print_coercion_tables", "testing.utils", -] +]] def is_unexpected(name): @@ -328,16 +340,12 @@ def is_unexpected(name): if '._' in name or '.tests' in name or '.setup' in name: return False - if name.startswith("numpy."): - name = name[6:] - if name in PUBLIC_MODULES: return False if name in PUBLIC_ALIASED_MODULES: return False - if name in PRIVATE_BUT_PRESENT_MODULES: return False @@ -390,10 +398,10 @@ def test_all_modules_are_expected_2(): print(obj) """ - modnames = [] - def check(modname): - module = importlib.import_module(modname) + def find_unexpected_members(mod_name): + members = [] + module = importlib.import_module(mod_name) if hasattr(module, '__all__'): objnames = module.__all__ else: @@ -401,15 +409,17 @@ def check(modname): for objname in objnames: if not objname.startswith('_'): - fullobjname = modname + '.' + objname - if isinstance(eval(fullobjname), types.ModuleType): + fullobjname = mod_name + '.' + objname + if isinstance(getattr(module, objname), types.ModuleType): if is_unexpected(fullobjname): - modnames.append(fullobjname) + members.append(fullobjname) + + return members - check("numpy") + unexpected_members = find_unexpected_members("numpy") for modname in PUBLIC_MODULES: - check("numpy." + modname) + unexpected_members.extend(find_unexpected_members(modname)) - if modnames: + if unexpected_members: raise AssertionError("Found unexpected object(s) that look like " - "modules: {}".format(modnames)) + "modules: {}".format(unexpected_members)) From 69bd8010edb30674f39d2fc3b7ae0a1d03b55c23 Mon Sep 17 00:00:00 2001 From: Ralf Gommers Date: Wed, 11 Sep 2019 20:30:53 -0700 Subject: [PATCH 08/10] TST: make xfailed test for modules in public API pass. --- numpy/lib/format.py | 3 +++ numpy/matlib.py | 2 +- numpy/tests/test_public_api.py | 38 +++++++++++++++++++++++++++++++--- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/numpy/lib/format.py b/numpy/lib/format.py index 3bf818812b78..1ecd72815316 100644 --- a/numpy/lib/format.py +++ b/numpy/lib/format.py @@ -173,6 +173,9 @@ ) +__all__ = [] + + MAGIC_PREFIX = b'\x93NUMPY' MAGIC_LEN = len(MAGIC_PREFIX) + 2 ARRAY_ALIGN = 64 # plausible values are powers of 2 between 16 and 4096 diff --git a/numpy/matlib.py b/numpy/matlib.py index 9e115943a217..604ef470b26e 100644 --- a/numpy/matlib.py +++ b/numpy/matlib.py @@ -2,7 +2,7 @@ import numpy as np from numpy.matrixlib.defmatrix import matrix, asmatrix -# need * as we're copying the numpy namespace +# need * as we're copying the numpy namespace (FIXME: this makes little sense) from numpy import * __version__ = np.__version__ diff --git a/numpy/tests/test_public_api.py b/numpy/tests/test_public_api.py index b6d8a4715b2f..716ddb3e3b5b 100644 --- a/numpy/tests/test_public_api.py +++ b/numpy/tests/test_public_api.py @@ -372,8 +372,39 @@ def test_all_modules_are_expected(): raise AssertionError("Found unexpected modules: {}".format(modnames)) -@pytest.mark.xfail(reason="missing __all__ dicts are messing this up, " - "needs work") +# Stuff that clearly shouldn't be in the API and is detected by the next test +# below +SKIP_LIST = [ + 'numpy.math', + 'numpy.distutils.log.sys', + 'numpy.distutils.system_info.copy', + 'numpy.distutils.system_info.distutils', + 'numpy.distutils.system_info.log', + 'numpy.distutils.system_info.os', + 'numpy.distutils.system_info.platform', + 'numpy.distutils.system_info.re', + 'numpy.distutils.system_info.shutil', + 'numpy.distutils.system_info.subprocess', + 'numpy.distutils.system_info.sys', + 'numpy.distutils.system_info.tempfile', + 'numpy.distutils.system_info.textwrap', + 'numpy.distutils.system_info.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 @@ -412,7 +443,8 @@ def find_unexpected_members(mod_name): fullobjname = mod_name + '.' + objname if isinstance(getattr(module, objname), types.ModuleType): if is_unexpected(fullobjname): - members.append(fullobjname) + if fullobjname not in SKIP_LIST: + members.append(fullobjname) return members From 2bc84dba9871eaa654153f855c9b74e88a55ff10 Mon Sep 17 00:00:00 2001 From: Ralf Gommers Date: Mon, 16 Sep 2019 14:49:15 -0700 Subject: [PATCH 09/10] TST: remove random.entropy from public API test, it was removed in gh-14498 --- numpy/tests/test_public_api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/numpy/tests/test_public_api.py b/numpy/tests/test_public_api.py index 716ddb3e3b5b..a9b478938385 100644 --- a/numpy/tests/test_public_api.py +++ b/numpy/tests/test_public_api.py @@ -319,7 +319,6 @@ def test_NPY_NO_EXPORT(): "random.bit_generator", "random.bounded_integers", "random.common", - "random.entropy", "random.generator", "random.info", "random.mt19937", From 9530d43f1b1314e3fc3b3c9175746849fe679f67 Mon Sep 17 00:00:00 2001 From: Ralf Gommers Date: Thu, 19 Sep 2019 09:42:28 +0200 Subject: [PATCH 10/10] TST: add test to check that (semi-)public API modules can be imported Also remove `numpy.ma.version.py`, it was not importable and served no purpose. --- numpy/ma/version.py | 14 ------ numpy/tests/test_public_api.py | 85 +++++++++++++++++++++++++++------- 2 files changed, 68 insertions(+), 31 deletions(-) delete mode 100644 numpy/ma/version.py diff --git a/numpy/ma/version.py b/numpy/ma/version.py deleted file mode 100644 index a2c5c42a806a..000000000000 --- a/numpy/ma/version.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Version number - -""" -from __future__ import division, absolute_import, print_function - -version = '1.00' -release = False - -if not release: - from . import core - from . import extras - revision = [core.__revision__.split(':')[-1][:-1].strip(), - extras.__revision__.split(':')[-1][:-1].strip(),] - version += '.dev%04i' % max([int(rev) for rev in revision]) diff --git a/numpy/tests/test_public_api.py b/numpy/tests/test_public_api.py index a9b478938385..e27d9b404187 100644 --- a/numpy/tests/test_public_api.py +++ b/numpy/tests/test_public_api.py @@ -5,6 +5,7 @@ import pkgutil import types import importlib +import warnings import numpy as np import numpy @@ -193,19 +194,10 @@ def test_NPY_NO_EXPORT(): "conftest", "core", "core.arrayprint", - "core.code_generators", - "core.code_generators.genapi", - "core.code_generators.generate_numpy_api", - "core.code_generators.generate_ufunc_api", - "core.code_generators.generate_umath", - "core.code_generators.numpy_api", - "core.code_generators.ufunc_docstrings", - "core.cversions", "core.defchararray", "core.einsumfunc", "core.fromnumeric", "core.function_base", - "core.generate_numpy_api", "core.getlimits", "core.info", "core.machar", @@ -264,7 +256,6 @@ def test_NPY_NO_EXPORT(): "distutils.lib2def", "distutils.line_endings", "distutils.mingw32ccompiler", - "distutils.msvc9compiler", "distutils.msvccompiler", "distutils.npy_pkg_config", "distutils.numpy_distribution", @@ -285,9 +276,6 @@ def test_NPY_NO_EXPORT(): "f2py.rules", "f2py.use_rules", "fft.helper", - "fft.info", - "fft.pocketfft", - "fft.pocketfft_internal", "lib.arraypad", "lib.arraysetops", "lib.arrayterator", @@ -313,7 +301,6 @@ def test_NPY_NO_EXPORT(): "ma.core", "ma.testutils", "ma.timer_comparison", - "ma.version", "matrixlib", "matrixlib.defmatrix", "random.bit_generator", @@ -351,6 +338,22 @@ def is_unexpected(name): 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.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 @@ -361,7 +364,7 @@ def test_all_modules_are_expected(): for _, modname, ispkg in pkgutil.walk_packages(path=np.__path__, prefix=np.__name__ + '.', onerror=None): - if is_unexpected(modname): + 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! @@ -373,7 +376,7 @@ def test_all_modules_are_expected(): # Stuff that clearly shouldn't be in the API and is detected by the next test # below -SKIP_LIST = [ +SKIP_LIST_2 = [ 'numpy.math', 'numpy.distutils.log.sys', 'numpy.distutils.system_info.copy', @@ -442,7 +445,7 @@ def find_unexpected_members(mod_name): fullobjname = mod_name + '.' + objname if isinstance(getattr(module, objname), types.ModuleType): if is_unexpected(fullobjname): - if fullobjname not in SKIP_LIST: + if fullobjname not in SKIP_LIST_2: members.append(fullobjname) return members @@ -454,3 +457,51 @@ def find_unexpected_members(mod_name): 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))