test_public_api.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. """
  2. This test script is adopted from:
  3. https://github.com/numpy/numpy/blob/main/numpy/tests/test_public_api.py
  4. """
  5. import pkgutil
  6. import types
  7. import importlib
  8. import warnings
  9. import scipy
  10. def check_dir(module, module_name=None):
  11. """Returns a mapping of all objects with the wrong __module__ attribute."""
  12. if module_name is None:
  13. module_name = module.__name__
  14. results = {}
  15. for name in dir(module):
  16. item = getattr(module, name)
  17. if (hasattr(item, '__module__') and hasattr(item, '__name__')
  18. and item.__module__ != module_name):
  19. results[name] = item.__module__ + '.' + item.__name__
  20. return results
  21. def test_dir_testing():
  22. """Assert that output of dir has only one "testing/tester"
  23. attribute without duplicate"""
  24. assert len(dir(scipy)) == len(set(dir(scipy)))
  25. # Historically SciPy has not used leading underscores for private submodules
  26. # much. This has resulted in lots of things that look like public modules
  27. # (i.e. things that can be imported as `import scipy.somesubmodule.somefile`),
  28. # but were never intended to be public. The PUBLIC_MODULES list contains
  29. # modules that are either public because they were meant to be, or because they
  30. # contain public functions/objects that aren't present in any other namespace
  31. # for whatever reason and therefore should be treated as public.
  32. PUBLIC_MODULES = ["scipy." + s for s in [
  33. "cluster",
  34. "cluster.vq",
  35. "cluster.hierarchy",
  36. "constants",
  37. "datasets",
  38. "fft",
  39. "fftpack",
  40. "integrate",
  41. "interpolate",
  42. "io",
  43. "io.arff",
  44. "io.matlab",
  45. "io.wavfile",
  46. "linalg",
  47. "linalg.blas",
  48. "linalg.cython_blas",
  49. "linalg.lapack",
  50. "linalg.cython_lapack",
  51. "linalg.interpolative",
  52. "misc",
  53. "ndimage",
  54. "odr",
  55. "optimize",
  56. "signal",
  57. "signal.windows",
  58. "sparse",
  59. "sparse.linalg",
  60. "sparse.csgraph",
  61. "spatial",
  62. "spatial.distance",
  63. "spatial.transform",
  64. "special",
  65. "stats",
  66. "stats.contingency",
  67. "stats.distributions",
  68. "stats.mstats",
  69. "stats.qmc",
  70. "stats.sampling"
  71. ]]
  72. # The PRIVATE_BUT_PRESENT_MODULES list contains modules that look public (lack
  73. # of underscores) but should not be used. For many of those modules the
  74. # current status is fine. For others it may make sense to work on making them
  75. # private, to clean up our public API and avoid confusion.
  76. # These private modules support will be removed in SciPy v2.0.0
  77. PRIVATE_BUT_PRESENT_MODULES = [
  78. 'scipy.constants.codata',
  79. 'scipy.constants.constants',
  80. 'scipy.fftpack.basic',
  81. 'scipy.fftpack.convolve',
  82. 'scipy.fftpack.helper',
  83. 'scipy.fftpack.pseudo_diffs',
  84. 'scipy.fftpack.realtransforms',
  85. 'scipy.integrate.odepack',
  86. 'scipy.integrate.quadpack',
  87. 'scipy.integrate.dop',
  88. 'scipy.integrate.lsoda',
  89. 'scipy.integrate.vode',
  90. 'scipy.interpolate.dfitpack',
  91. 'scipy.interpolate.fitpack',
  92. 'scipy.interpolate.fitpack2',
  93. 'scipy.interpolate.interpnd',
  94. 'scipy.interpolate.interpolate',
  95. 'scipy.interpolate.ndgriddata',
  96. 'scipy.interpolate.polyint',
  97. 'scipy.interpolate.rbf',
  98. 'scipy.io.arff.arffread',
  99. 'scipy.io.harwell_boeing',
  100. 'scipy.io.idl',
  101. 'scipy.io.mmio',
  102. 'scipy.io.netcdf',
  103. 'scipy.io.matlab.byteordercodes',
  104. 'scipy.io.matlab.mio',
  105. 'scipy.io.matlab.mio4',
  106. 'scipy.io.matlab.mio5',
  107. 'scipy.io.matlab.mio5_params',
  108. 'scipy.io.matlab.mio5_utils',
  109. 'scipy.io.matlab.mio_utils',
  110. 'scipy.io.matlab.miobase',
  111. 'scipy.io.matlab.streams',
  112. 'scipy.linalg.basic',
  113. 'scipy.linalg.decomp',
  114. 'scipy.linalg.decomp_cholesky',
  115. 'scipy.linalg.decomp_lu',
  116. 'scipy.linalg.decomp_qr',
  117. 'scipy.linalg.decomp_schur',
  118. 'scipy.linalg.decomp_svd',
  119. 'scipy.linalg.flinalg',
  120. 'scipy.linalg.matfuncs',
  121. 'scipy.linalg.misc',
  122. 'scipy.linalg.special_matrices',
  123. 'scipy.misc.common',
  124. 'scipy.misc.doccer',
  125. 'scipy.ndimage.filters',
  126. 'scipy.ndimage.fourier',
  127. 'scipy.ndimage.interpolation',
  128. 'scipy.ndimage.measurements',
  129. 'scipy.ndimage.morphology',
  130. 'scipy.odr.models',
  131. 'scipy.odr.odrpack',
  132. 'scipy.optimize.cobyla',
  133. 'scipy.optimize.cython_optimize',
  134. 'scipy.optimize.lbfgsb',
  135. 'scipy.optimize.linesearch',
  136. 'scipy.optimize.minpack',
  137. 'scipy.optimize.minpack2',
  138. 'scipy.optimize.moduleTNC',
  139. 'scipy.optimize.nonlin',
  140. 'scipy.optimize.optimize',
  141. 'scipy.optimize.slsqp',
  142. 'scipy.optimize.tnc',
  143. 'scipy.optimize.zeros',
  144. 'scipy.signal.bsplines',
  145. 'scipy.signal.filter_design',
  146. 'scipy.signal.fir_filter_design',
  147. 'scipy.signal.lti_conversion',
  148. 'scipy.signal.ltisys',
  149. 'scipy.signal.signaltools',
  150. 'scipy.signal.spectral',
  151. 'scipy.signal.spline',
  152. 'scipy.signal.waveforms',
  153. 'scipy.signal.wavelets',
  154. 'scipy.signal.windows.windows',
  155. 'scipy.sparse.base',
  156. 'scipy.sparse.bsr',
  157. 'scipy.sparse.compressed',
  158. 'scipy.sparse.construct',
  159. 'scipy.sparse.coo',
  160. 'scipy.sparse.csc',
  161. 'scipy.sparse.csr',
  162. 'scipy.sparse.data',
  163. 'scipy.sparse.dia',
  164. 'scipy.sparse.dok',
  165. 'scipy.sparse.extract',
  166. 'scipy.sparse.lil',
  167. 'scipy.sparse.linalg.dsolve',
  168. 'scipy.sparse.linalg.eigen',
  169. 'scipy.sparse.linalg.interface',
  170. 'scipy.sparse.linalg.isolve',
  171. 'scipy.sparse.linalg.matfuncs',
  172. 'scipy.sparse.sparsetools',
  173. 'scipy.sparse.spfuncs',
  174. 'scipy.sparse.sputils',
  175. 'scipy.spatial.ckdtree',
  176. 'scipy.spatial.kdtree',
  177. 'scipy.spatial.qhull',
  178. 'scipy.spatial.transform.rotation',
  179. 'scipy.special.add_newdocs',
  180. 'scipy.special.basic',
  181. 'scipy.special.cython_special',
  182. 'scipy.special.orthogonal',
  183. 'scipy.special.sf_error',
  184. 'scipy.special.specfun',
  185. 'scipy.special.spfun_stats',
  186. 'scipy.stats.biasedurn',
  187. 'scipy.stats.kde',
  188. 'scipy.stats.morestats',
  189. 'scipy.stats.mstats_basic',
  190. 'scipy.stats.mstats_extras',
  191. 'scipy.stats.mvn',
  192. 'scipy.stats.statlib',
  193. 'scipy.stats.stats',
  194. ]
  195. def is_unexpected(name):
  196. """Check if this needs to be considered."""
  197. if '._' in name or '.tests' in name or '.setup' in name:
  198. return False
  199. if name in PUBLIC_MODULES:
  200. return False
  201. if name in PRIVATE_BUT_PRESENT_MODULES:
  202. return False
  203. return True
  204. SKIP_LIST = [
  205. 'scipy.conftest',
  206. 'scipy.version',
  207. ]
  208. def test_all_modules_are_expected():
  209. """
  210. Test that we don't add anything that looks like a new public module by
  211. accident. Check is based on filenames.
  212. """
  213. modnames = []
  214. for _, modname, ispkg in pkgutil.walk_packages(path=scipy.__path__,
  215. prefix=scipy.__name__ + '.',
  216. onerror=None):
  217. if is_unexpected(modname) and modname not in SKIP_LIST:
  218. # We have a name that is new. If that's on purpose, add it to
  219. # PUBLIC_MODULES. We don't expect to have to add anything to
  220. # PRIVATE_BUT_PRESENT_MODULES. Use an underscore in the name!
  221. modnames.append(modname)
  222. if modnames:
  223. raise AssertionError(f'Found unexpected modules: {modnames}')
  224. # Stuff that clearly shouldn't be in the API and is detected by the next test
  225. # below
  226. SKIP_LIST_2 = [
  227. 'scipy.char',
  228. 'scipy.rec',
  229. 'scipy.emath',
  230. 'scipy.math',
  231. 'scipy.random',
  232. 'scipy.ctypeslib',
  233. 'scipy.ma'
  234. ]
  235. def test_all_modules_are_expected_2():
  236. """
  237. Method checking all objects. The pkgutil-based method in
  238. `test_all_modules_are_expected` does not catch imports into a namespace,
  239. only filenames.
  240. """
  241. def find_unexpected_members(mod_name):
  242. members = []
  243. module = importlib.import_module(mod_name)
  244. if hasattr(module, '__all__'):
  245. objnames = module.__all__
  246. else:
  247. objnames = dir(module)
  248. for objname in objnames:
  249. if not objname.startswith('_'):
  250. fullobjname = mod_name + '.' + objname
  251. if isinstance(getattr(module, objname), types.ModuleType):
  252. if is_unexpected(fullobjname) and fullobjname not in SKIP_LIST_2:
  253. members.append(fullobjname)
  254. return members
  255. unexpected_members = find_unexpected_members("scipy")
  256. for modname in PUBLIC_MODULES:
  257. unexpected_members.extend(find_unexpected_members(modname))
  258. if unexpected_members:
  259. raise AssertionError("Found unexpected object(s) that look like "
  260. "modules: {}".format(unexpected_members))
  261. def test_api_importable():
  262. """
  263. Check that all submodules listed higher up in this file can be imported
  264. Note that if a PRIVATE_BUT_PRESENT_MODULES entry goes missing, it may
  265. simply need to be removed from the list (deprecation may or may not be
  266. needed - apply common sense).
  267. """
  268. def check_importable(module_name):
  269. try:
  270. importlib.import_module(module_name)
  271. except (ImportError, AttributeError):
  272. return False
  273. return True
  274. module_names = []
  275. for module_name in PUBLIC_MODULES:
  276. if not check_importable(module_name):
  277. module_names.append(module_name)
  278. if module_names:
  279. raise AssertionError("Modules in the public API that cannot be "
  280. "imported: {}".format(module_names))
  281. with warnings.catch_warnings(record=True) as w:
  282. warnings.filterwarnings('always', category=DeprecationWarning)
  283. warnings.filterwarnings('always', category=ImportWarning)
  284. for module_name in PRIVATE_BUT_PRESENT_MODULES:
  285. if not check_importable(module_name):
  286. module_names.append(module_name)
  287. if module_names:
  288. raise AssertionError("Modules that are not really public but looked "
  289. "public and can not be imported: "
  290. "{}".format(module_names))