test_public_api.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810
  1. import functools
  2. import sys
  3. import sysconfig
  4. import subprocess
  5. import pkgutil
  6. import types
  7. import importlib
  8. import inspect
  9. import warnings
  10. import numpy as np
  11. import numpy
  12. from numpy.testing import IS_WASM
  13. import pytest
  14. try:
  15. import ctypes
  16. except ImportError:
  17. ctypes = None
  18. def check_dir(module, module_name=None):
  19. """Returns a mapping of all objects with the wrong __module__ attribute."""
  20. if module_name is None:
  21. module_name = module.__name__
  22. results = {}
  23. for name in dir(module):
  24. if name == "core":
  25. continue
  26. item = getattr(module, name)
  27. if (hasattr(item, '__module__') and hasattr(item, '__name__')
  28. and item.__module__ != module_name):
  29. results[name] = item.__module__ + '.' + item.__name__
  30. return results
  31. def test_numpy_namespace():
  32. # We override dir to not show these members
  33. allowlist = {
  34. 'recarray': 'numpy.rec.recarray',
  35. }
  36. bad_results = check_dir(np)
  37. # pytest gives better error messages with the builtin assert than with
  38. # assert_equal
  39. assert bad_results == allowlist
  40. @pytest.mark.skipif(IS_WASM, reason="can't start subprocess")
  41. @pytest.mark.parametrize('name', ['testing'])
  42. def test_import_lazy_import(name):
  43. """Make sure we can actually use the modules we lazy load.
  44. While not exported as part of the public API, it was accessible. With the
  45. use of __getattr__ and __dir__, this isn't always true It can happen that
  46. an infinite recursion may happen.
  47. This is the only way I found that would force the failure to appear on the
  48. badly implemented code.
  49. We also test for the presence of the lazily imported modules in dir
  50. """
  51. exe = (sys.executable, '-c', "import numpy; numpy." + name)
  52. result = subprocess.check_output(exe)
  53. assert not result
  54. # Make sure they are still in the __dir__
  55. assert name in dir(np)
  56. def test_dir_testing():
  57. """Assert that output of dir has only one "testing/tester"
  58. attribute without duplicate"""
  59. assert len(dir(np)) == len(set(dir(np)))
  60. def test_numpy_linalg():
  61. bad_results = check_dir(np.linalg)
  62. assert bad_results == {}
  63. def test_numpy_fft():
  64. bad_results = check_dir(np.fft)
  65. assert bad_results == {}
  66. @pytest.mark.skipif(ctypes is None,
  67. reason="ctypes not available in this python")
  68. def test_NPY_NO_EXPORT():
  69. cdll = ctypes.CDLL(np._core._multiarray_tests.__file__)
  70. # Make sure an arbitrary NPY_NO_EXPORT function is actually hidden
  71. f = getattr(cdll, 'test_not_exported', None)
  72. assert f is None, ("'test_not_exported' is mistakenly exported, "
  73. "NPY_NO_EXPORT does not work")
  74. # Historically NumPy has not used leading underscores for private submodules
  75. # much. This has resulted in lots of things that look like public modules
  76. # (i.e. things that can be imported as `import numpy.somesubmodule.somefile`),
  77. # but were never intended to be public. The PUBLIC_MODULES list contains
  78. # modules that are either public because they were meant to be, or because they
  79. # contain public functions/objects that aren't present in any other namespace
  80. # for whatever reason and therefore should be treated as public.
  81. #
  82. # The PRIVATE_BUT_PRESENT_MODULES list contains modules that look public (lack
  83. # of underscores) but should not be used. For many of those modules the
  84. # current status is fine. For others it may make sense to work on making them
  85. # private, to clean up our public API and avoid confusion.
  86. PUBLIC_MODULES = ['numpy.' + s for s in [
  87. "ctypeslib",
  88. "dtypes",
  89. "exceptions",
  90. "f2py",
  91. "fft",
  92. "lib",
  93. "lib.array_utils",
  94. "lib.format",
  95. "lib.introspect",
  96. "lib.mixins",
  97. "lib.npyio",
  98. "lib.recfunctions", # note: still needs cleaning, was forgotten for 2.0
  99. "lib.scimath",
  100. "lib.stride_tricks",
  101. "linalg",
  102. "ma",
  103. "ma.extras",
  104. "ma.mrecords",
  105. "polynomial",
  106. "polynomial.chebyshev",
  107. "polynomial.hermite",
  108. "polynomial.hermite_e",
  109. "polynomial.laguerre",
  110. "polynomial.legendre",
  111. "polynomial.polynomial",
  112. "random",
  113. "strings",
  114. "testing",
  115. "testing.overrides",
  116. "typing",
  117. "typing.mypy_plugin",
  118. "version",
  119. ]]
  120. if sys.version_info < (3, 12):
  121. PUBLIC_MODULES += [
  122. 'numpy.' + s for s in [
  123. "distutils",
  124. "distutils.cpuinfo",
  125. "distutils.exec_command",
  126. "distutils.misc_util",
  127. "distutils.log",
  128. "distutils.system_info",
  129. ]
  130. ]
  131. PUBLIC_ALIASED_MODULES = [
  132. "numpy.char",
  133. "numpy.emath",
  134. "numpy.rec",
  135. ]
  136. PRIVATE_BUT_PRESENT_MODULES = ['numpy.' + s for s in [
  137. "compat",
  138. "compat.py3k",
  139. "conftest",
  140. "core",
  141. "core.multiarray",
  142. "core.numeric",
  143. "core.umath",
  144. "core.arrayprint",
  145. "core.defchararray",
  146. "core.einsumfunc",
  147. "core.fromnumeric",
  148. "core.function_base",
  149. "core.getlimits",
  150. "core.numerictypes",
  151. "core.overrides",
  152. "core.records",
  153. "core.shape_base",
  154. "f2py.auxfuncs",
  155. "f2py.capi_maps",
  156. "f2py.cb_rules",
  157. "f2py.cfuncs",
  158. "f2py.common_rules",
  159. "f2py.crackfortran",
  160. "f2py.diagnose",
  161. "f2py.f2py2e",
  162. "f2py.f90mod_rules",
  163. "f2py.func2subr",
  164. "f2py.rules",
  165. "f2py.symbolic",
  166. "f2py.use_rules",
  167. "fft.helper",
  168. "lib.user_array", # note: not in np.lib, but probably should just be deleted
  169. "linalg.lapack_lite",
  170. "linalg.linalg",
  171. "ma.core",
  172. "ma.testutils",
  173. "ma.timer_comparison",
  174. "matlib",
  175. "matrixlib",
  176. "matrixlib.defmatrix",
  177. "polynomial.polyutils",
  178. "random.mtrand",
  179. "random.bit_generator",
  180. "testing.print_coercion_tables",
  181. ]]
  182. if sys.version_info < (3, 12):
  183. PRIVATE_BUT_PRESENT_MODULES += [
  184. 'numpy.' + s for s in [
  185. "distutils.armccompiler",
  186. "distutils.fujitsuccompiler",
  187. "distutils.ccompiler",
  188. 'distutils.ccompiler_opt',
  189. "distutils.command",
  190. "distutils.command.autodist",
  191. "distutils.command.bdist_rpm",
  192. "distutils.command.build",
  193. "distutils.command.build_clib",
  194. "distutils.command.build_ext",
  195. "distutils.command.build_py",
  196. "distutils.command.build_scripts",
  197. "distutils.command.build_src",
  198. "distutils.command.config",
  199. "distutils.command.config_compiler",
  200. "distutils.command.develop",
  201. "distutils.command.egg_info",
  202. "distutils.command.install",
  203. "distutils.command.install_clib",
  204. "distutils.command.install_data",
  205. "distutils.command.install_headers",
  206. "distutils.command.sdist",
  207. "distutils.conv_template",
  208. "distutils.core",
  209. "distutils.extension",
  210. "distutils.fcompiler",
  211. "distutils.fcompiler.absoft",
  212. "distutils.fcompiler.arm",
  213. "distutils.fcompiler.compaq",
  214. "distutils.fcompiler.environment",
  215. "distutils.fcompiler.g95",
  216. "distutils.fcompiler.gnu",
  217. "distutils.fcompiler.hpux",
  218. "distutils.fcompiler.ibm",
  219. "distutils.fcompiler.intel",
  220. "distutils.fcompiler.lahey",
  221. "distutils.fcompiler.mips",
  222. "distutils.fcompiler.nag",
  223. "distutils.fcompiler.none",
  224. "distutils.fcompiler.pathf95",
  225. "distutils.fcompiler.pg",
  226. "distutils.fcompiler.nv",
  227. "distutils.fcompiler.sun",
  228. "distutils.fcompiler.vast",
  229. "distutils.fcompiler.fujitsu",
  230. "distutils.from_template",
  231. "distutils.intelccompiler",
  232. "distutils.lib2def",
  233. "distutils.line_endings",
  234. "distutils.mingw32ccompiler",
  235. "distutils.msvccompiler",
  236. "distutils.npy_pkg_config",
  237. "distutils.numpy_distribution",
  238. "distutils.pathccompiler",
  239. "distutils.unixccompiler",
  240. ]
  241. ]
  242. def is_unexpected(name):
  243. """Check if this needs to be considered."""
  244. if '._' in name or '.tests' in name or '.setup' in name:
  245. return False
  246. if name in PUBLIC_MODULES:
  247. return False
  248. if name in PUBLIC_ALIASED_MODULES:
  249. return False
  250. if name in PRIVATE_BUT_PRESENT_MODULES:
  251. return False
  252. return True
  253. if sys.version_info < (3, 12):
  254. SKIP_LIST = ["numpy.distutils.msvc9compiler"]
  255. else:
  256. SKIP_LIST = []
  257. # suppressing warnings from deprecated modules
  258. @pytest.mark.filterwarnings("ignore:.*np.compat.*:DeprecationWarning")
  259. def test_all_modules_are_expected():
  260. """
  261. Test that we don't add anything that looks like a new public module by
  262. accident. Check is based on filenames.
  263. """
  264. modnames = []
  265. for _, modname, ispkg in pkgutil.walk_packages(path=np.__path__,
  266. prefix=np.__name__ + '.',
  267. onerror=None):
  268. if is_unexpected(modname) and modname not in SKIP_LIST:
  269. # We have a name that is new. If that's on purpose, add it to
  270. # PUBLIC_MODULES. We don't expect to have to add anything to
  271. # PRIVATE_BUT_PRESENT_MODULES. Use an underscore in the name!
  272. modnames.append(modname)
  273. if modnames:
  274. raise AssertionError(f'Found unexpected modules: {modnames}')
  275. # Stuff that clearly shouldn't be in the API and is detected by the next test
  276. # below
  277. SKIP_LIST_2 = [
  278. 'numpy.lib.math',
  279. 'numpy.matlib.char',
  280. 'numpy.matlib.rec',
  281. 'numpy.matlib.emath',
  282. 'numpy.matlib.exceptions',
  283. 'numpy.matlib.math',
  284. 'numpy.matlib.linalg',
  285. 'numpy.matlib.fft',
  286. 'numpy.matlib.random',
  287. 'numpy.matlib.ctypeslib',
  288. 'numpy.matlib.ma',
  289. ]
  290. if sys.version_info < (3, 12):
  291. SKIP_LIST_2 += [
  292. 'numpy.distutils.log.sys',
  293. 'numpy.distutils.log.logging',
  294. 'numpy.distutils.log.warnings',
  295. ]
  296. def test_all_modules_are_expected_2():
  297. """
  298. Method checking all objects. The pkgutil-based method in
  299. `test_all_modules_are_expected` does not catch imports into a namespace,
  300. only filenames. So this test is more thorough, and checks this like:
  301. import .lib.scimath as emath
  302. To check if something in a module is (effectively) public, one can check if
  303. there's anything in that namespace that's a public function/object but is
  304. not exposed in a higher-level namespace. For example for a `numpy.lib`
  305. submodule::
  306. mod = np.lib.mixins
  307. for obj in mod.__all__:
  308. if obj in np.__all__:
  309. continue
  310. elif obj in np.lib.__all__:
  311. continue
  312. else:
  313. print(obj)
  314. """
  315. def find_unexpected_members(mod_name):
  316. members = []
  317. module = importlib.import_module(mod_name)
  318. if hasattr(module, '__all__'):
  319. objnames = module.__all__
  320. else:
  321. objnames = dir(module)
  322. for objname in objnames:
  323. if not objname.startswith('_'):
  324. fullobjname = mod_name + '.' + objname
  325. if isinstance(getattr(module, objname), types.ModuleType):
  326. if is_unexpected(fullobjname):
  327. if fullobjname not in SKIP_LIST_2:
  328. members.append(fullobjname)
  329. return members
  330. unexpected_members = find_unexpected_members("numpy")
  331. for modname in PUBLIC_MODULES:
  332. unexpected_members.extend(find_unexpected_members(modname))
  333. if unexpected_members:
  334. raise AssertionError("Found unexpected object(s) that look like "
  335. "modules: {}".format(unexpected_members))
  336. def test_api_importable():
  337. """
  338. Check that all submodules listed higher up in this file can be imported
  339. Note that if a PRIVATE_BUT_PRESENT_MODULES entry goes missing, it may
  340. simply need to be removed from the list (deprecation may or may not be
  341. needed - apply common sense).
  342. """
  343. def check_importable(module_name):
  344. try:
  345. importlib.import_module(module_name)
  346. except (ImportError, AttributeError):
  347. return False
  348. return True
  349. module_names = []
  350. for module_name in PUBLIC_MODULES:
  351. if not check_importable(module_name):
  352. module_names.append(module_name)
  353. if module_names:
  354. raise AssertionError("Modules in the public API that cannot be "
  355. "imported: {}".format(module_names))
  356. for module_name in PUBLIC_ALIASED_MODULES:
  357. try:
  358. eval(module_name)
  359. except AttributeError:
  360. module_names.append(module_name)
  361. if module_names:
  362. raise AssertionError("Modules in the public API that were not "
  363. "found: {}".format(module_names))
  364. with warnings.catch_warnings(record=True) as w:
  365. warnings.filterwarnings('always', category=DeprecationWarning)
  366. warnings.filterwarnings('always', category=ImportWarning)
  367. for module_name in PRIVATE_BUT_PRESENT_MODULES:
  368. if not check_importable(module_name):
  369. module_names.append(module_name)
  370. if module_names:
  371. raise AssertionError("Modules that are not really public but looked "
  372. "public and can not be imported: "
  373. "{}".format(module_names))
  374. @pytest.mark.xfail(
  375. sysconfig.get_config_var("Py_DEBUG") not in (None, 0, "0"),
  376. reason=(
  377. "NumPy possibly built with `USE_DEBUG=True ./tools/travis-test.sh`, "
  378. "which does not expose the `array_api` entry point. "
  379. "See https://github.com/numpy/numpy/pull/19800"
  380. ),
  381. )
  382. def test_array_api_entry_point():
  383. """
  384. Entry point for Array API implementation can be found with importlib and
  385. returns the main numpy namespace.
  386. """
  387. # For a development install that did not go through meson-python,
  388. # the entrypoint will not have been installed. So ensure this test fails
  389. # only if numpy is inside site-packages.
  390. numpy_in_sitepackages = sysconfig.get_path('platlib') in np.__file__
  391. eps = importlib.metadata.entry_points()
  392. try:
  393. xp_eps = eps.select(group="array_api")
  394. except AttributeError:
  395. # The select interface for entry_points was introduced in py3.10,
  396. # deprecating its dict interface. We fallback to dict keys for finding
  397. # Array API entry points so that running this test in <=3.9 will
  398. # still work - see https://github.com/numpy/numpy/pull/19800.
  399. xp_eps = eps.get("array_api", [])
  400. if len(xp_eps) == 0:
  401. if numpy_in_sitepackages:
  402. msg = "No entry points for 'array_api' found"
  403. raise AssertionError(msg) from None
  404. return
  405. try:
  406. ep = next(ep for ep in xp_eps if ep.name == "numpy")
  407. except StopIteration:
  408. if numpy_in_sitepackages:
  409. msg = "'numpy' not in array_api entry points"
  410. raise AssertionError(msg) from None
  411. return
  412. if ep.value == 'numpy.array_api':
  413. # Looks like the entrypoint for the current numpy build isn't
  414. # installed, but an older numpy is also installed and hence the
  415. # entrypoint is pointing to the old (no longer existing) location.
  416. # This isn't a problem except for when running tests with `spin` or an
  417. # in-place build.
  418. return
  419. xp = ep.load()
  420. msg = (
  421. f"numpy entry point value '{ep.value}' "
  422. "does not point to our Array API implementation"
  423. )
  424. assert xp is numpy, msg
  425. def test_main_namespace_all_dir_coherence():
  426. """
  427. Checks if `dir(np)` and `np.__all__` are consistent and return
  428. the same content, excluding exceptions and private members.
  429. """
  430. def _remove_private_members(member_set):
  431. return {m for m in member_set if not m.startswith('_')}
  432. def _remove_exceptions(member_set):
  433. return member_set.difference({
  434. "bool" # included only in __dir__
  435. })
  436. all_members = _remove_private_members(np.__all__)
  437. all_members = _remove_exceptions(all_members)
  438. dir_members = _remove_private_members(np.__dir__())
  439. dir_members = _remove_exceptions(dir_members)
  440. assert all_members == dir_members, (
  441. "Members that break symmetry: "
  442. f"{all_members.symmetric_difference(dir_members)}"
  443. )
  444. @pytest.mark.filterwarnings(
  445. r"ignore:numpy.core(\.\w+)? is deprecated:DeprecationWarning"
  446. )
  447. def test_core_shims_coherence():
  448. """
  449. Check that all "semi-public" members of `numpy._core` are also accessible
  450. from `numpy.core` shims.
  451. """
  452. import numpy.core as core
  453. for member_name in dir(np._core):
  454. # Skip private and test members. Also if a module is aliased,
  455. # no need to add it to np.core
  456. if (
  457. member_name.startswith("_")
  458. or member_name in ["tests", "strings"]
  459. or f"numpy.{member_name}" in PUBLIC_ALIASED_MODULES
  460. ):
  461. continue
  462. member = getattr(np._core, member_name)
  463. # np.core is a shim and all submodules of np.core are shims
  464. # but we should be able to import everything in those shims
  465. # that are available in the "real" modules in np._core
  466. if inspect.ismodule(member):
  467. submodule = member
  468. submodule_name = member_name
  469. for submodule_member_name in dir(submodule):
  470. # ignore dunder names
  471. if submodule_member_name.startswith("__"):
  472. continue
  473. submodule_member = getattr(submodule, submodule_member_name)
  474. core_submodule = __import__(
  475. f"numpy.core.{submodule_name}",
  476. fromlist=[submodule_member_name]
  477. )
  478. assert submodule_member is getattr(
  479. core_submodule, submodule_member_name
  480. )
  481. else:
  482. assert member is getattr(core, member_name)
  483. def test_functions_single_location():
  484. """
  485. Check that each public function is available from one location only.
  486. Test performs BFS search traversing NumPy's public API. It flags
  487. any function-like object that is accessible from more that one place.
  488. """
  489. from typing import Any, Callable, Dict, List, Set, Tuple
  490. from numpy._core._multiarray_umath import (
  491. _ArrayFunctionDispatcher as dispatched_function
  492. )
  493. visited_modules: Set[types.ModuleType] = {np}
  494. visited_functions: Set[Callable[..., Any]] = set()
  495. # Functions often have `__name__` overridden, therefore we need
  496. # to keep track of locations where functions have been found.
  497. functions_original_paths: Dict[Callable[..., Any], str] = dict()
  498. # Here we aggregate functions with more than one location.
  499. # It must be empty for the test to pass.
  500. duplicated_functions: List[Tuple] = []
  501. modules_queue = [np]
  502. while len(modules_queue) > 0:
  503. module = modules_queue.pop()
  504. for member_name in dir(module):
  505. member = getattr(module, member_name)
  506. # first check if we got a module
  507. if (
  508. inspect.ismodule(member) and # it's a module
  509. "numpy" in member.__name__ and # inside NumPy
  510. not member_name.startswith("_") and # not private
  511. "numpy._core" not in member.__name__ and # outside _core
  512. # not a legacy or testing module
  513. member_name not in ["f2py", "ma", "testing", "tests"] and
  514. member not in visited_modules # not visited yet
  515. ):
  516. modules_queue.append(member)
  517. visited_modules.add(member)
  518. # else check if we got a function-like object
  519. elif (
  520. inspect.isfunction(member) or
  521. isinstance(member, (dispatched_function, np.ufunc))
  522. ):
  523. if member in visited_functions:
  524. # skip main namespace functions with aliases
  525. if (
  526. member.__name__ in [
  527. "absolute", # np.abs
  528. "arccos", # np.acos
  529. "arccosh", # np.acosh
  530. "arcsin", # np.asin
  531. "arcsinh", # np.asinh
  532. "arctan", # np.atan
  533. "arctan2", # np.atan2
  534. "arctanh", # np.atanh
  535. "left_shift", # np.bitwise_left_shift
  536. "right_shift", # np.bitwise_right_shift
  537. "conjugate", # np.conj
  538. "invert", # np.bitwise_not & np.bitwise_invert
  539. "remainder", # np.mod
  540. "divide", # np.true_divide
  541. "concatenate", # np.concat
  542. "power", # np.pow
  543. "transpose", # np.permute_dims
  544. ] and
  545. module.__name__ == "numpy"
  546. ):
  547. continue
  548. # skip trimcoef from numpy.polynomial as it is
  549. # duplicated by design.
  550. if (
  551. member.__name__ == "trimcoef" and
  552. module.__name__.startswith("numpy.polynomial")
  553. ):
  554. continue
  555. # skip ufuncs that are exported in np.strings as well
  556. if member.__name__ in (
  557. "add",
  558. "equal",
  559. "not_equal",
  560. "greater",
  561. "greater_equal",
  562. "less",
  563. "less_equal",
  564. ) and module.__name__ == "numpy.strings":
  565. continue
  566. # numpy.char reexports all numpy.strings functions for
  567. # backwards-compatibility
  568. if module.__name__ == "numpy.char":
  569. continue
  570. # function is present in more than one location!
  571. duplicated_functions.append(
  572. (member.__name__,
  573. module.__name__,
  574. functions_original_paths[member])
  575. )
  576. else:
  577. visited_functions.add(member)
  578. functions_original_paths[member] = module.__name__
  579. del visited_functions, visited_modules, functions_original_paths
  580. assert len(duplicated_functions) == 0, duplicated_functions
  581. def test___module___attribute():
  582. modules_queue = [np]
  583. visited_modules = {np}
  584. visited_functions = set()
  585. incorrect_entries = []
  586. while len(modules_queue) > 0:
  587. module = modules_queue.pop()
  588. for member_name in dir(module):
  589. member = getattr(module, member_name)
  590. # first check if we got a module
  591. if (
  592. inspect.ismodule(member) and # it's a module
  593. "numpy" in member.__name__ and # inside NumPy
  594. not member_name.startswith("_") and # not private
  595. "numpy._core" not in member.__name__ and # outside _core
  596. # not in a skip module list
  597. member_name not in [
  598. "char", "core", "ctypeslib", "f2py", "ma", "lapack_lite",
  599. "mrecords", "testing", "tests", "polynomial", "typing",
  600. "mtrand", "bit_generator",
  601. ] and
  602. member not in visited_modules # not visited yet
  603. ):
  604. modules_queue.append(member)
  605. visited_modules.add(member)
  606. elif (
  607. not inspect.ismodule(member) and
  608. hasattr(member, "__name__") and
  609. not member.__name__.startswith("_") and
  610. member.__module__ != module.__name__ and
  611. member not in visited_functions
  612. ):
  613. # skip ufuncs that are exported in np.strings as well
  614. if member.__name__ in (
  615. "add", "equal", "not_equal", "greater", "greater_equal",
  616. "less", "less_equal",
  617. ) and module.__name__ == "numpy.strings":
  618. continue
  619. # recarray and record are exported in np and np.rec
  620. if (
  621. (member.__name__ == "recarray" and module.__name__ == "numpy") or
  622. (member.__name__ == "record" and module.__name__ == "numpy.rec")
  623. ):
  624. continue
  625. # skip cdef classes
  626. if member.__name__ in (
  627. "BitGenerator", "Generator", "MT19937", "PCG64", "PCG64DXSM",
  628. "Philox", "RandomState", "SFC64", "SeedSequence",
  629. ):
  630. continue
  631. incorrect_entries.append(
  632. dict(
  633. Func=member.__name__,
  634. actual=member.__module__,
  635. expected=module.__name__,
  636. )
  637. )
  638. visited_functions.add(member)
  639. if incorrect_entries:
  640. assert len(incorrect_entries) == 0, incorrect_entries
  641. def _check___qualname__(obj) -> bool:
  642. qualname = obj.__qualname__
  643. name = obj.__name__
  644. module_name = obj.__module__
  645. assert name == qualname.split(".")[-1]
  646. module = sys.modules[module_name]
  647. actual_obj = functools.reduce(getattr, qualname.split("."), module)
  648. return (
  649. actual_obj is obj or
  650. (
  651. # for bound methods check qualname match
  652. module_name.startswith("numpy.random") and
  653. actual_obj.__qualname__ == qualname
  654. )
  655. )
  656. def test___qualname___attribute():
  657. modules_queue = [np]
  658. visited_modules = {np}
  659. visited_functions = set()
  660. incorrect_entries = []
  661. while len(modules_queue) > 0:
  662. module = modules_queue.pop()
  663. for member_name in dir(module):
  664. member = getattr(module, member_name)
  665. # first check if we got a module
  666. if (
  667. inspect.ismodule(member) and # it's a module
  668. "numpy" in member.__name__ and # inside NumPy
  669. not member_name.startswith("_") and # not private
  670. member_name not in [
  671. "f2py", "ma", "tests", "testing", "typing",
  672. "bit_generator", "ctypeslib", "lapack_lite",
  673. ] and # skip modules
  674. "numpy._core" not in member.__name__ and # outside _core
  675. member not in visited_modules # not visited yet
  676. ):
  677. modules_queue.append(member)
  678. visited_modules.add(member)
  679. elif (
  680. not inspect.ismodule(member) and
  681. hasattr(member, "__name__") and
  682. not member.__name__.startswith("_") and
  683. not member_name.startswith("_") and
  684. not _check___qualname__(member) and
  685. member not in visited_functions
  686. ):
  687. incorrect_entries.append(
  688. dict(
  689. actual=member.__qualname__, expected=member.__name__,
  690. )
  691. )
  692. visited_functions.add(member)
  693. if incorrect_entries:
  694. assert len(incorrect_entries) == 0, incorrect_entries