util.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. """
  2. Utility functions for
  3. - building and importing modules on test time, using a temporary location
  4. - detecting if compilers are present
  5. - determining paths to tests
  6. """
  7. import glob
  8. import os
  9. import sys
  10. import subprocess
  11. import tempfile
  12. import shutil
  13. import atexit
  14. import textwrap
  15. import re
  16. import pytest
  17. import contextlib
  18. import numpy
  19. from pathlib import Path
  20. from numpy._utils import asunicode
  21. from numpy.testing import temppath, IS_WASM
  22. from importlib import import_module
  23. #
  24. # Maintaining a temporary module directory
  25. #
  26. _module_dir = None
  27. _module_num = 5403
  28. if sys.platform == "cygwin":
  29. NUMPY_INSTALL_ROOT = Path(__file__).parent.parent.parent
  30. _module_list = list(NUMPY_INSTALL_ROOT.glob("**/*.dll"))
  31. def _cleanup():
  32. global _module_dir
  33. if _module_dir is not None:
  34. try:
  35. sys.path.remove(_module_dir)
  36. except ValueError:
  37. pass
  38. try:
  39. shutil.rmtree(_module_dir)
  40. except OSError:
  41. pass
  42. _module_dir = None
  43. def get_module_dir():
  44. global _module_dir
  45. if _module_dir is None:
  46. _module_dir = tempfile.mkdtemp()
  47. atexit.register(_cleanup)
  48. if _module_dir not in sys.path:
  49. sys.path.insert(0, _module_dir)
  50. return _module_dir
  51. def get_temp_module_name():
  52. # Assume single-threaded, and the module dir usable only by this thread
  53. global _module_num
  54. get_module_dir()
  55. name = "_test_ext_module_%d" % _module_num
  56. _module_num += 1
  57. if name in sys.modules:
  58. # this should not be possible, but check anyway
  59. raise RuntimeError("Temporary module name already in use.")
  60. return name
  61. def _memoize(func):
  62. memo = {}
  63. def wrapper(*a, **kw):
  64. key = repr((a, kw))
  65. if key not in memo:
  66. try:
  67. memo[key] = func(*a, **kw)
  68. except Exception as e:
  69. memo[key] = e
  70. raise
  71. ret = memo[key]
  72. if isinstance(ret, Exception):
  73. raise ret
  74. return ret
  75. wrapper.__name__ = func.__name__
  76. return wrapper
  77. #
  78. # Building modules
  79. #
  80. @_memoize
  81. def build_module(source_files, options=[], skip=[], only=[], module_name=None):
  82. """
  83. Compile and import a f2py module, built from the given files.
  84. """
  85. code = f"import sys; sys.path = {sys.path!r}; import numpy.f2py; numpy.f2py.main()"
  86. d = get_module_dir()
  87. # Copy files
  88. dst_sources = []
  89. f2py_sources = []
  90. for fn in source_files:
  91. if not os.path.isfile(fn):
  92. raise RuntimeError("%s is not a file" % fn)
  93. dst = os.path.join(d, os.path.basename(fn))
  94. shutil.copyfile(fn, dst)
  95. dst_sources.append(dst)
  96. base, ext = os.path.splitext(dst)
  97. if ext in (".f90", ".f", ".c", ".pyf"):
  98. f2py_sources.append(dst)
  99. assert f2py_sources
  100. # Prepare options
  101. if module_name is None:
  102. module_name = get_temp_module_name()
  103. f2py_opts = ["-c", "-m", module_name] + options + f2py_sources
  104. if skip:
  105. f2py_opts += ["skip:"] + skip
  106. if only:
  107. f2py_opts += ["only:"] + only
  108. # Build
  109. cwd = os.getcwd()
  110. try:
  111. os.chdir(d)
  112. cmd = [sys.executable, "-c", code] + f2py_opts
  113. p = subprocess.Popen(cmd,
  114. stdout=subprocess.PIPE,
  115. stderr=subprocess.STDOUT)
  116. out, err = p.communicate()
  117. if p.returncode != 0:
  118. raise RuntimeError("Running f2py failed: %s\n%s" %
  119. (cmd[4:], asunicode(out)))
  120. finally:
  121. os.chdir(cwd)
  122. # Partial cleanup
  123. for fn in dst_sources:
  124. os.unlink(fn)
  125. # Rebase (Cygwin-only)
  126. if sys.platform == "cygwin":
  127. # If someone starts deleting modules after import, this will
  128. # need to change to record how big each module is, rather than
  129. # relying on rebase being able to find that from the files.
  130. _module_list.extend(
  131. glob.glob(os.path.join(d, "{:s}*".format(module_name)))
  132. )
  133. subprocess.check_call(
  134. ["/usr/bin/rebase", "--database", "--oblivious", "--verbose"]
  135. + _module_list
  136. )
  137. # Import
  138. return import_module(module_name)
  139. @_memoize
  140. def build_code(source_code,
  141. options=[],
  142. skip=[],
  143. only=[],
  144. suffix=None,
  145. module_name=None):
  146. """
  147. Compile and import Fortran code using f2py.
  148. """
  149. if suffix is None:
  150. suffix = ".f"
  151. with temppath(suffix=suffix) as path:
  152. with open(path, "w") as f:
  153. f.write(source_code)
  154. return build_module([path],
  155. options=options,
  156. skip=skip,
  157. only=only,
  158. module_name=module_name)
  159. #
  160. # Check if compilers are available at all...
  161. #
  162. _compiler_status = None
  163. def _get_compiler_status():
  164. global _compiler_status
  165. if _compiler_status is not None:
  166. return _compiler_status
  167. _compiler_status = (False, False, False)
  168. if IS_WASM:
  169. # Can't run compiler from inside WASM.
  170. return _compiler_status
  171. # XXX: this is really ugly. But I don't know how to invoke Distutils
  172. # in a safer way...
  173. code = textwrap.dedent(f"""\
  174. import os
  175. import sys
  176. sys.path = {repr(sys.path)}
  177. def configuration(parent_name='',top_path=None):
  178. global config
  179. from numpy.distutils.misc_util import Configuration
  180. config = Configuration('', parent_name, top_path)
  181. return config
  182. from numpy.distutils.core import setup
  183. setup(configuration=configuration)
  184. config_cmd = config.get_config_cmd()
  185. have_c = config_cmd.try_compile('void foo() {{}}')
  186. print('COMPILERS:%%d,%%d,%%d' %% (have_c,
  187. config.have_f77c(),
  188. config.have_f90c()))
  189. sys.exit(99)
  190. """)
  191. code = code % dict(syspath=repr(sys.path))
  192. tmpdir = tempfile.mkdtemp()
  193. try:
  194. script = os.path.join(tmpdir, "setup.py")
  195. with open(script, "w") as f:
  196. f.write(code)
  197. cmd = [sys.executable, "setup.py", "config"]
  198. p = subprocess.Popen(cmd,
  199. stdout=subprocess.PIPE,
  200. stderr=subprocess.STDOUT,
  201. cwd=tmpdir)
  202. out, err = p.communicate()
  203. finally:
  204. shutil.rmtree(tmpdir)
  205. m = re.search(br"COMPILERS:(\d+),(\d+),(\d+)", out)
  206. if m:
  207. _compiler_status = (
  208. bool(int(m.group(1))),
  209. bool(int(m.group(2))),
  210. bool(int(m.group(3))),
  211. )
  212. # Finished
  213. return _compiler_status
  214. def has_c_compiler():
  215. return _get_compiler_status()[0]
  216. def has_f77_compiler():
  217. return _get_compiler_status()[1]
  218. def has_f90_compiler():
  219. return _get_compiler_status()[2]
  220. #
  221. # Building with distutils
  222. #
  223. @_memoize
  224. def build_module_distutils(source_files, config_code, module_name, **kw):
  225. """
  226. Build a module via distutils and import it.
  227. """
  228. d = get_module_dir()
  229. # Copy files
  230. dst_sources = []
  231. for fn in source_files:
  232. if not os.path.isfile(fn):
  233. raise RuntimeError("%s is not a file" % fn)
  234. dst = os.path.join(d, os.path.basename(fn))
  235. shutil.copyfile(fn, dst)
  236. dst_sources.append(dst)
  237. # Build script
  238. config_code = textwrap.dedent(config_code).replace("\n", "\n ")
  239. code = fr"""
  240. import os
  241. import sys
  242. sys.path = {repr(sys.path)}
  243. def configuration(parent_name='',top_path=None):
  244. from numpy.distutils.misc_util import Configuration
  245. config = Configuration('', parent_name, top_path)
  246. {config_code}
  247. return config
  248. if __name__ == "__main__":
  249. from numpy.distutils.core import setup
  250. setup(configuration=configuration)
  251. """
  252. script = os.path.join(d, get_temp_module_name() + ".py")
  253. dst_sources.append(script)
  254. with open(script, "wb") as f:
  255. f.write(code.encode('latin1'))
  256. # Build
  257. cwd = os.getcwd()
  258. try:
  259. os.chdir(d)
  260. cmd = [sys.executable, script, "build_ext", "-i"]
  261. p = subprocess.Popen(cmd,
  262. stdout=subprocess.PIPE,
  263. stderr=subprocess.STDOUT)
  264. out, err = p.communicate()
  265. if p.returncode != 0:
  266. raise RuntimeError("Running distutils build failed: %s\n%s" %
  267. (cmd[4:], asstr(out)))
  268. finally:
  269. os.chdir(cwd)
  270. # Partial cleanup
  271. for fn in dst_sources:
  272. os.unlink(fn)
  273. # Import
  274. __import__(module_name)
  275. return sys.modules[module_name]
  276. #
  277. # Unittest convenience
  278. #
  279. class F2PyTest:
  280. code = None
  281. sources = None
  282. options = []
  283. skip = []
  284. only = []
  285. suffix = ".f"
  286. module = None
  287. @property
  288. def module_name(self):
  289. cls = type(self)
  290. return f'_{cls.__module__.rsplit(".",1)[-1]}_{cls.__name__}_ext_module'
  291. def setup_method(self):
  292. if sys.platform == "win32":
  293. pytest.skip("Fails with MinGW64 Gfortran (Issue #9673)")
  294. if self.module is not None:
  295. return
  296. # Check compiler availability first
  297. if not has_c_compiler():
  298. pytest.skip("No C compiler available")
  299. codes = []
  300. if self.sources:
  301. codes.extend(self.sources)
  302. if self.code is not None:
  303. codes.append(self.suffix)
  304. needs_f77 = False
  305. needs_f90 = False
  306. needs_pyf = False
  307. for fn in codes:
  308. if str(fn).endswith(".f"):
  309. needs_f77 = True
  310. elif str(fn).endswith(".f90"):
  311. needs_f90 = True
  312. elif str(fn).endswith(".pyf"):
  313. needs_pyf = True
  314. if needs_f77 and not has_f77_compiler():
  315. pytest.skip("No Fortran 77 compiler available")
  316. if needs_f90 and not has_f90_compiler():
  317. pytest.skip("No Fortran 90 compiler available")
  318. if needs_pyf and not (has_f90_compiler() or has_f77_compiler()):
  319. pytest.skip("No Fortran compiler available")
  320. # Build the module
  321. if self.code is not None:
  322. self.module = build_code(
  323. self.code,
  324. options=self.options,
  325. skip=self.skip,
  326. only=self.only,
  327. suffix=self.suffix,
  328. module_name=self.module_name,
  329. )
  330. if self.sources is not None:
  331. self.module = build_module(
  332. self.sources,
  333. options=self.options,
  334. skip=self.skip,
  335. only=self.only,
  336. module_name=self.module_name,
  337. )
  338. #
  339. # Helper functions
  340. #
  341. def getpath(*a):
  342. # Package root
  343. d = Path(numpy.f2py.__file__).parent.resolve()
  344. return d.joinpath(*a)
  345. @contextlib.contextmanager
  346. def switchdir(path):
  347. curpath = Path.cwd()
  348. os.chdir(path)
  349. try:
  350. yield
  351. finally:
  352. os.chdir(curpath)