util.py 10 KB

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