util.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. """
  2. Utility functions for
  3. - building and importing modules on test time, using a temporary location
  4. - detecting if compilers are present
  5. """
  6. from __future__ import division, absolute_import, print_function
  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. from numpy.compat import asbytes, asstr
  17. from numpy.testing import temppath
  18. from importlib import import_module
  19. try:
  20. from hashlib import md5
  21. except ImportError:
  22. from md5 import new as md5 # noqa: F401
  23. #
  24. # Maintaining a temporary module directory
  25. #
  26. _module_dir = None
  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 (IOError, 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. d = get_module_dir()
  50. for j in range(5403, 9999999):
  51. name = "_test_ext_module_%d" % j
  52. fn = os.path.join(d, name)
  53. if name not in sys.modules and not os.path.isfile(fn + '.py'):
  54. return name
  55. raise RuntimeError("Failed to create a temporary module name")
  56. def _memoize(func):
  57. memo = {}
  58. def wrapper(*a, **kw):
  59. key = repr((a, kw))
  60. if key not in memo:
  61. try:
  62. memo[key] = func(*a, **kw)
  63. except Exception as e:
  64. memo[key] = e
  65. raise
  66. ret = memo[key]
  67. if isinstance(ret, Exception):
  68. raise ret
  69. return ret
  70. wrapper.__name__ = func.__name__
  71. return wrapper
  72. #
  73. # Building modules
  74. #
  75. @_memoize
  76. def build_module(source_files, options=[], skip=[], only=[], module_name=None):
  77. """
  78. Compile and import a f2py module, built from the given files.
  79. """
  80. code = ("import sys; sys.path = %s; import numpy.f2py as f2py2e; "
  81. "f2py2e.main()" % repr(sys.path))
  82. d = get_module_dir()
  83. # Copy files
  84. dst_sources = []
  85. for fn in source_files:
  86. if not os.path.isfile(fn):
  87. raise RuntimeError("%s is not a file" % fn)
  88. dst = os.path.join(d, os.path.basename(fn))
  89. shutil.copyfile(fn, dst)
  90. dst_sources.append(dst)
  91. fn = os.path.join(os.path.dirname(fn), '.f2py_f2cmap')
  92. if os.path.isfile(fn):
  93. dst = os.path.join(d, os.path.basename(fn))
  94. if not os.path.isfile(dst):
  95. shutil.copyfile(fn, dst)
  96. # Prepare options
  97. if module_name is None:
  98. module_name = get_temp_module_name()
  99. f2py_opts = ['-c', '-m', module_name] + options + dst_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, stdout=subprocess.PIPE,
  110. stderr=subprocess.STDOUT)
  111. out, err = p.communicate()
  112. if p.returncode != 0:
  113. raise RuntimeError("Running f2py failed: %s\n%s"
  114. % (cmd[4:], asstr(out)))
  115. finally:
  116. os.chdir(cwd)
  117. # Partial cleanup
  118. for fn in dst_sources:
  119. os.unlink(fn)
  120. # Import
  121. return import_module(module_name)
  122. @_memoize
  123. def build_code(source_code, options=[], skip=[], only=[], suffix=None,
  124. module_name=None):
  125. """
  126. Compile and import Fortran code using f2py.
  127. """
  128. if suffix is None:
  129. suffix = '.f'
  130. with temppath(suffix=suffix) as path:
  131. with open(path, 'w') as f:
  132. f.write(source_code)
  133. return build_module([path], options=options, skip=skip, only=only,
  134. module_name=module_name)
  135. #
  136. # Check if compilers are available at all...
  137. #
  138. _compiler_status = None
  139. def _get_compiler_status():
  140. global _compiler_status
  141. if _compiler_status is not None:
  142. return _compiler_status
  143. _compiler_status = (False, False, False)
  144. # XXX: this is really ugly. But I don't know how to invoke Distutils
  145. # in a safer way...
  146. code = """
  147. import os
  148. import sys
  149. sys.path = %(syspath)s
  150. def configuration(parent_name='',top_path=None):
  151. global config
  152. from numpy.distutils.misc_util import Configuration
  153. config = Configuration('', parent_name, top_path)
  154. return config
  155. from numpy.distutils.core import setup
  156. setup(configuration=configuration)
  157. config_cmd = config.get_config_cmd()
  158. have_c = config_cmd.try_compile('void foo() {}')
  159. print('COMPILERS:%%d,%%d,%%d' %% (have_c,
  160. config.have_f77c(),
  161. config.have_f90c()))
  162. sys.exit(99)
  163. """
  164. code = code % dict(syspath=repr(sys.path))
  165. with temppath(suffix='.py') as script:
  166. with open(script, 'w') as f:
  167. f.write(code)
  168. cmd = [sys.executable, script, 'config']
  169. p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
  170. stderr=subprocess.STDOUT)
  171. out, err = p.communicate()
  172. m = re.search(br'COMPILERS:(\d+),(\d+),(\d+)', out)
  173. if m:
  174. _compiler_status = (bool(int(m.group(1))), bool(int(m.group(2))),
  175. bool(int(m.group(3))))
  176. # Finished
  177. return _compiler_status
  178. def has_c_compiler():
  179. return _get_compiler_status()[0]
  180. def has_f77_compiler():
  181. return _get_compiler_status()[1]
  182. def has_f90_compiler():
  183. return _get_compiler_status()[2]
  184. #
  185. # Building with distutils
  186. #
  187. @_memoize
  188. def build_module_distutils(source_files, config_code, module_name, **kw):
  189. """
  190. Build a module via distutils and import it.
  191. """
  192. from numpy.distutils.misc_util import Configuration
  193. from numpy.distutils.core import setup
  194. d = get_module_dir()
  195. # Copy files
  196. dst_sources = []
  197. for fn in source_files:
  198. if not os.path.isfile(fn):
  199. raise RuntimeError("%s is not a file" % fn)
  200. dst = os.path.join(d, os.path.basename(fn))
  201. shutil.copyfile(fn, dst)
  202. dst_sources.append(dst)
  203. # Build script
  204. config_code = textwrap.dedent(config_code).replace("\n", "\n ")
  205. code = """\
  206. import os
  207. import sys
  208. sys.path = %(syspath)s
  209. def configuration(parent_name='',top_path=None):
  210. from numpy.distutils.misc_util import Configuration
  211. config = Configuration('', parent_name, top_path)
  212. %(config_code)s
  213. return config
  214. if __name__ == "__main__":
  215. from numpy.distutils.core import setup
  216. setup(configuration=configuration)
  217. """ % dict(config_code=config_code, syspath=repr(sys.path))
  218. script = os.path.join(d, get_temp_module_name() + '.py')
  219. dst_sources.append(script)
  220. f = open(script, 'wb')
  221. f.write(asbytes(code))
  222. f.close()
  223. # Build
  224. cwd = os.getcwd()
  225. try:
  226. os.chdir(d)
  227. cmd = [sys.executable, script, 'build_ext', '-i']
  228. p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
  229. stderr=subprocess.STDOUT)
  230. out, err = p.communicate()
  231. if p.returncode != 0:
  232. raise RuntimeError("Running distutils build failed: %s\n%s"
  233. % (cmd[4:], asstr(out)))
  234. finally:
  235. os.chdir(cwd)
  236. # Partial cleanup
  237. for fn in dst_sources:
  238. os.unlink(fn)
  239. # Import
  240. __import__(module_name)
  241. return sys.modules[module_name]
  242. #
  243. # Unittest convenience
  244. #
  245. class F2PyTest(object):
  246. code = None
  247. sources = None
  248. options = []
  249. skip = []
  250. only = []
  251. suffix = '.f'
  252. module = None
  253. module_name = None
  254. def setup(self):
  255. if sys.platform == 'win32':
  256. pytest.skip('Fails with MinGW64 Gfortran (Issue #9673)')
  257. if self.module is not None:
  258. return
  259. # Check compiler availability first
  260. if not has_c_compiler():
  261. pytest.skip("No C compiler available")
  262. codes = []
  263. if self.sources:
  264. codes.extend(self.sources)
  265. if self.code is not None:
  266. codes.append(self.suffix)
  267. needs_f77 = False
  268. needs_f90 = False
  269. for fn in codes:
  270. if fn.endswith('.f'):
  271. needs_f77 = True
  272. elif fn.endswith('.f90'):
  273. needs_f90 = True
  274. if needs_f77 and not has_f77_compiler():
  275. pytest.skip("No Fortran 77 compiler available")
  276. if needs_f90 and not has_f90_compiler():
  277. pytest.skip("No Fortran 90 compiler available")
  278. # Build the module
  279. if self.code is not None:
  280. self.module = build_code(self.code, options=self.options,
  281. skip=self.skip, only=self.only,
  282. suffix=self.suffix,
  283. module_name=self.module_name)
  284. if self.sources is not None:
  285. self.module = build_module(self.sources, options=self.options,
  286. skip=self.skip, only=self.only,
  287. module_name=self.module_name)