test_public_api.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. from __future__ import division, absolute_import, print_function
  2. import sys
  3. import subprocess
  4. import pkgutil
  5. import types
  6. import importlib
  7. import warnings
  8. import numpy as np
  9. import numpy
  10. import pytest
  11. try:
  12. import ctypes
  13. except ImportError:
  14. ctypes = None
  15. def check_dir(module, module_name=None):
  16. """Returns a mapping of all objects with the wrong __module__ attribute."""
  17. if module_name is None:
  18. module_name = module.__name__
  19. results = {}
  20. for name in dir(module):
  21. item = getattr(module, name)
  22. if (hasattr(item, '__module__') and hasattr(item, '__name__')
  23. and item.__module__ != module_name):
  24. results[name] = item.__module__ + '.' + item.__name__
  25. return results
  26. @pytest.mark.skipif(
  27. sys.version_info[0] < 3,
  28. reason="NumPy exposes slightly different functions on Python 2")
  29. def test_numpy_namespace():
  30. # None of these objects are publicly documented to be part of the main
  31. # NumPy namespace (some are useful though, others need to be cleaned up)
  32. undocumented = {
  33. 'Tester': 'numpy.testing._private.nosetester.NoseTester',
  34. '_add_newdoc_ufunc': 'numpy.core._multiarray_umath._add_newdoc_ufunc',
  35. 'add_docstring': 'numpy.core._multiarray_umath.add_docstring',
  36. 'add_newdoc': 'numpy.core.function_base.add_newdoc',
  37. 'add_newdoc_ufunc': 'numpy.core._multiarray_umath._add_newdoc_ufunc',
  38. 'byte_bounds': 'numpy.lib.utils.byte_bounds',
  39. 'compare_chararrays': 'numpy.core._multiarray_umath.compare_chararrays',
  40. 'deprecate': 'numpy.lib.utils.deprecate',
  41. 'deprecate_with_doc': 'numpy.lib.utils.<lambda>',
  42. 'disp': 'numpy.lib.function_base.disp',
  43. 'fastCopyAndTranspose': 'numpy.core._multiarray_umath._fastCopyAndTranspose',
  44. 'get_array_wrap': 'numpy.lib.shape_base.get_array_wrap',
  45. 'get_include': 'numpy.lib.utils.get_include',
  46. 'int_asbuffer': 'numpy.core._multiarray_umath.int_asbuffer',
  47. 'mafromtxt': 'numpy.lib.npyio.mafromtxt',
  48. 'ndfromtxt': 'numpy.lib.npyio.ndfromtxt',
  49. 'recfromcsv': 'numpy.lib.npyio.recfromcsv',
  50. 'recfromtxt': 'numpy.lib.npyio.recfromtxt',
  51. 'safe_eval': 'numpy.lib.utils.safe_eval',
  52. 'set_string_function': 'numpy.core.arrayprint.set_string_function',
  53. 'show_config': 'numpy.__config__.show',
  54. 'who': 'numpy.lib.utils.who',
  55. }
  56. # These built-in types are re-exported by numpy.
  57. builtins = {
  58. 'bool': 'builtins.bool',
  59. 'complex': 'builtins.complex',
  60. 'float': 'builtins.float',
  61. 'int': 'builtins.int',
  62. 'long': 'builtins.int',
  63. 'object': 'builtins.object',
  64. 'str': 'builtins.str',
  65. 'unicode': 'builtins.str',
  66. }
  67. whitelist = dict(undocumented, **builtins)
  68. bad_results = check_dir(np)
  69. # pytest gives better error messages with the builtin assert than with
  70. # assert_equal
  71. assert bad_results == whitelist
  72. @pytest.mark.parametrize('name', ['testing', 'Tester'])
  73. def test_import_lazy_import(name):
  74. """Make sure we can actually use the modules we lazy load.
  75. While not exported as part of the public API, it was accessible. With the
  76. use of __getattr__ and __dir__, this isn't always true It can happen that
  77. an infinite recursion may happen.
  78. This is the only way I found that would force the failure to appear on the
  79. badly implemented code.
  80. We also test for the presence of the lazily imported modules in dir
  81. """
  82. exe = (sys.executable, '-c', "import numpy; numpy." + name)
  83. result = subprocess.check_output(exe)
  84. assert not result
  85. # Make sure they are still in the __dir__
  86. assert name in dir(np)
  87. def test_numpy_linalg():
  88. bad_results = check_dir(np.linalg)
  89. assert bad_results == {}
  90. def test_numpy_fft():
  91. bad_results = check_dir(np.fft)
  92. assert bad_results == {}
  93. @pytest.mark.skipif(ctypes is None,
  94. reason="ctypes not available in this python")
  95. def test_NPY_NO_EXPORT():
  96. cdll = ctypes.CDLL(np.core._multiarray_tests.__file__)
  97. # Make sure an arbitrary NPY_NO_EXPORT function is actually hidden
  98. f = getattr(cdll, 'test_not_exported', None)
  99. assert f is None, ("'test_not_exported' is mistakenly exported, "
  100. "NPY_NO_EXPORT does not work")
  101. # Historically NumPy has not used leading underscores for private submodules
  102. # much. This has resulted in lots of things that look like public modules
  103. # (i.e. things that can be imported as `import numpy.somesubmodule.somefile`),
  104. # but were never intended to be public. The PUBLIC_MODULES list contains
  105. # modules that are either public because they were meant to be, or because they
  106. # contain public functions/objects that aren't present in any other namespace
  107. # for whatever reason and therefore should be treated as public.
  108. #
  109. # The PRIVATE_BUT_PRESENT_MODULES list contains modules that look public (lack
  110. # of underscores) but should not be used. For many of those modules the
  111. # current status is fine. For others it may make sense to work on making them
  112. # private, to clean up our public API and avoid confusion.
  113. PUBLIC_MODULES = ['numpy.' + s for s in [
  114. "ctypeslib",
  115. "distutils",
  116. "distutils.cpuinfo",
  117. "distutils.exec_command",
  118. "distutils.misc_util",
  119. "distutils.log",
  120. "distutils.system_info",
  121. "doc",
  122. "doc.basics",
  123. "doc.broadcasting",
  124. "doc.byteswapping",
  125. "doc.constants",
  126. "doc.creation",
  127. "doc.dispatch",
  128. "doc.glossary",
  129. "doc.indexing",
  130. "doc.internals",
  131. "doc.misc",
  132. "doc.structured_arrays",
  133. "doc.subclassing",
  134. "doc.ufuncs",
  135. "dual",
  136. "f2py",
  137. "fft",
  138. "lib",
  139. "lib.format", # was this meant to be public?
  140. "lib.mixins",
  141. "lib.recfunctions",
  142. "lib.scimath",
  143. "linalg",
  144. "ma",
  145. "ma.extras",
  146. "ma.mrecords",
  147. "matlib",
  148. "polynomial",
  149. "polynomial.chebyshev",
  150. "polynomial.hermite",
  151. "polynomial.hermite_e",
  152. "polynomial.laguerre",
  153. "polynomial.legendre",
  154. "polynomial.polynomial",
  155. "polynomial.polyutils",
  156. "random",
  157. "testing",
  158. "version",
  159. ]]
  160. PUBLIC_ALIASED_MODULES = [
  161. "numpy.char",
  162. "numpy.emath",
  163. "numpy.rec",
  164. ]
  165. PRIVATE_BUT_PRESENT_MODULES = ['numpy.' + s for s in [
  166. "compat",
  167. "compat.py3k",
  168. "conftest",
  169. "core",
  170. "core.arrayprint",
  171. "core.defchararray",
  172. "core.einsumfunc",
  173. "core.fromnumeric",
  174. "core.function_base",
  175. "core.getlimits",
  176. "core.machar",
  177. "core.memmap",
  178. "core.multiarray",
  179. "core.numeric",
  180. "core.numerictypes",
  181. "core.overrides",
  182. "core.records",
  183. "core.shape_base",
  184. "core.umath",
  185. "core.umath_tests",
  186. "distutils.ccompiler",
  187. "distutils.command",
  188. "distutils.command.autodist",
  189. "distutils.command.bdist_rpm",
  190. "distutils.command.build",
  191. "distutils.command.build_clib",
  192. "distutils.command.build_ext",
  193. "distutils.command.build_py",
  194. "distutils.command.build_scripts",
  195. "distutils.command.build_src",
  196. "distutils.command.config",
  197. "distutils.command.config_compiler",
  198. "distutils.command.develop",
  199. "distutils.command.egg_info",
  200. "distutils.command.install",
  201. "distutils.command.install_clib",
  202. "distutils.command.install_data",
  203. "distutils.command.install_headers",
  204. "distutils.command.sdist",
  205. "distutils.compat",
  206. "distutils.conv_template",
  207. "distutils.core",
  208. "distutils.extension",
  209. "distutils.fcompiler",
  210. "distutils.fcompiler.absoft",
  211. "distutils.fcompiler.compaq",
  212. "distutils.fcompiler.environment",
  213. "distutils.fcompiler.g95",
  214. "distutils.fcompiler.gnu",
  215. "distutils.fcompiler.hpux",
  216. "distutils.fcompiler.ibm",
  217. "distutils.fcompiler.intel",
  218. "distutils.fcompiler.lahey",
  219. "distutils.fcompiler.mips",
  220. "distutils.fcompiler.nag",
  221. "distutils.fcompiler.none",
  222. "distutils.fcompiler.pathf95",
  223. "distutils.fcompiler.pg",
  224. "distutils.fcompiler.sun",
  225. "distutils.fcompiler.vast",
  226. "distutils.from_template",
  227. "distutils.intelccompiler",
  228. "distutils.lib2def",
  229. "distutils.line_endings",
  230. "distutils.mingw32ccompiler",
  231. "distutils.msvccompiler",
  232. "distutils.npy_pkg_config",
  233. "distutils.numpy_distribution",
  234. "distutils.pathccompiler",
  235. "distutils.unixccompiler",
  236. "f2py.auxfuncs",
  237. "f2py.capi_maps",
  238. "f2py.cb_rules",
  239. "f2py.cfuncs",
  240. "f2py.common_rules",
  241. "f2py.crackfortran",
  242. "f2py.diagnose",
  243. "f2py.f2py2e",
  244. "f2py.f2py_testing",
  245. "f2py.f90mod_rules",
  246. "f2py.func2subr",
  247. "f2py.rules",
  248. "f2py.use_rules",
  249. "fft.helper",
  250. "lib.arraypad",
  251. "lib.arraysetops",
  252. "lib.arrayterator",
  253. "lib.financial",
  254. "lib.function_base",
  255. "lib.histograms",
  256. "lib.index_tricks",
  257. "lib.nanfunctions",
  258. "lib.npyio",
  259. "lib.polynomial",
  260. "lib.shape_base",
  261. "lib.stride_tricks",
  262. "lib.twodim_base",
  263. "lib.type_check",
  264. "lib.ufunclike",
  265. "lib.user_array", # note: not in np.lib, but probably should just be deleted
  266. "lib.utils",
  267. "linalg.lapack_lite",
  268. "linalg.linalg",
  269. "ma.bench",
  270. "ma.core",
  271. "ma.testutils",
  272. "ma.timer_comparison",
  273. "matrixlib",
  274. "matrixlib.defmatrix",
  275. "random.mtrand",
  276. "testing.print_coercion_tables",
  277. "testing.utils",
  278. ]]
  279. def is_unexpected(name):
  280. """Check if this needs to be considered."""
  281. if '._' in name or '.tests' in name or '.setup' in name:
  282. return False
  283. if name in PUBLIC_MODULES:
  284. return False
  285. if name in PUBLIC_ALIASED_MODULES:
  286. return False
  287. if name in PRIVATE_BUT_PRESENT_MODULES:
  288. return False
  289. return True
  290. # These are present in a directory with an __init__.py but cannot be imported
  291. # code_generators/ isn't installed, but present for an inplace build
  292. SKIP_LIST = [
  293. "numpy.core.code_generators",
  294. "numpy.core.code_generators.genapi",
  295. "numpy.core.code_generators.generate_umath",
  296. "numpy.core.code_generators.ufunc_docstrings",
  297. "numpy.core.code_generators.generate_numpy_api",
  298. "numpy.core.code_generators.generate_ufunc_api",
  299. "numpy.core.code_generators.numpy_api",
  300. "numpy.core.cversions",
  301. "numpy.core.generate_numpy_api",
  302. "numpy.distutils.msvc9compiler",
  303. ]
  304. def test_all_modules_are_expected():
  305. """
  306. Test that we don't add anything that looks like a new public module by
  307. accident. Check is based on filenames.
  308. """
  309. modnames = []
  310. for _, modname, ispkg in pkgutil.walk_packages(path=np.__path__,
  311. prefix=np.__name__ + '.',
  312. onerror=None):
  313. if is_unexpected(modname) and modname not in SKIP_LIST:
  314. # We have a name that is new. If that's on purpose, add it to
  315. # PUBLIC_MODULES. We don't expect to have to add anything to
  316. # PRIVATE_BUT_PRESENT_MODULES. Use an underscore in the name!
  317. modnames.append(modname)
  318. if modnames:
  319. raise AssertionError("Found unexpected modules: {}".format(modnames))
  320. # Stuff that clearly shouldn't be in the API and is detected by the next test
  321. # below
  322. SKIP_LIST_2 = [
  323. 'numpy.math',
  324. 'numpy.distutils.log.sys',
  325. 'numpy.distutils.system_info.copy',
  326. 'numpy.distutils.system_info.distutils',
  327. 'numpy.distutils.system_info.log',
  328. 'numpy.distutils.system_info.os',
  329. 'numpy.distutils.system_info.platform',
  330. 'numpy.distutils.system_info.re',
  331. 'numpy.distutils.system_info.shutil',
  332. 'numpy.distutils.system_info.subprocess',
  333. 'numpy.distutils.system_info.sys',
  334. 'numpy.distutils.system_info.tempfile',
  335. 'numpy.distutils.system_info.textwrap',
  336. 'numpy.distutils.system_info.warnings',
  337. 'numpy.doc.constants.re',
  338. 'numpy.doc.constants.textwrap',
  339. 'numpy.lib.emath',
  340. 'numpy.lib.math',
  341. 'numpy.matlib.char',
  342. 'numpy.matlib.rec',
  343. 'numpy.matlib.emath',
  344. 'numpy.matlib.math',
  345. 'numpy.matlib.linalg',
  346. 'numpy.matlib.fft',
  347. 'numpy.matlib.random',
  348. 'numpy.matlib.ctypeslib',
  349. 'numpy.matlib.ma',
  350. ]
  351. def test_all_modules_are_expected_2():
  352. """
  353. Method checking all objects. The pkgutil-based method in
  354. `test_all_modules_are_expected` does not catch imports into a namespace,
  355. only filenames. So this test is more thorough, and checks this like:
  356. import .lib.scimath as emath
  357. To check if something in a module is (effectively) public, one can check if
  358. there's anything in that namespace that's a public function/object but is
  359. not exposed in a higher-level namespace. For example for a `numpy.lib`
  360. submodule::
  361. mod = np.lib.mixins
  362. for obj in mod.__all__:
  363. if obj in np.__all__:
  364. continue
  365. elif obj in np.lib.__all__:
  366. continue
  367. else:
  368. print(obj)
  369. """
  370. def find_unexpected_members(mod_name):
  371. members = []
  372. module = importlib.import_module(mod_name)
  373. if hasattr(module, '__all__'):
  374. objnames = module.__all__
  375. else:
  376. objnames = dir(module)
  377. for objname in objnames:
  378. if not objname.startswith('_'):
  379. fullobjname = mod_name + '.' + objname
  380. if isinstance(getattr(module, objname), types.ModuleType):
  381. if is_unexpected(fullobjname):
  382. if fullobjname not in SKIP_LIST_2:
  383. members.append(fullobjname)
  384. return members
  385. unexpected_members = find_unexpected_members("numpy")
  386. for modname in PUBLIC_MODULES:
  387. unexpected_members.extend(find_unexpected_members(modname))
  388. if unexpected_members:
  389. raise AssertionError("Found unexpected object(s) that look like "
  390. "modules: {}".format(unexpected_members))
  391. def test_api_importable():
  392. """
  393. Check that all submodules listed higher up in this file can be imported
  394. Note that if a PRIVATE_BUT_PRESENT_MODULES entry goes missing, it may
  395. simply need to be removed from the list (deprecation may or may not be
  396. needed - apply common sense).
  397. """
  398. def check_importable(module_name):
  399. try:
  400. importlib.import_module(module_name)
  401. except (ImportError, AttributeError):
  402. return False
  403. return True
  404. module_names = []
  405. for module_name in PUBLIC_MODULES:
  406. if not check_importable(module_name):
  407. module_names.append(module_name)
  408. if module_names:
  409. raise AssertionError("Modules in the public API that cannot be "
  410. "imported: {}".format(module_names))
  411. for module_name in PUBLIC_ALIASED_MODULES:
  412. try:
  413. eval(module_name)
  414. except AttributeError:
  415. module_names.append(module_name)
  416. if module_names:
  417. raise AssertionError("Modules in the public API that were not "
  418. "found: {}".format(module_names))
  419. with warnings.catch_warnings(record=True) as w:
  420. warnings.filterwarnings('always', category=DeprecationWarning)
  421. warnings.filterwarnings('always', category=ImportWarning)
  422. for module_name in PRIVATE_BUT_PRESENT_MODULES:
  423. if not check_importable(module_name):
  424. module_names.append(module_name)
  425. if module_names:
  426. raise AssertionError("Modules that are not really public but looked "
  427. "public and can not be imported: "
  428. "{}".format(module_names))