util.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  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 atexit
  8. import concurrent.futures
  9. import contextlib
  10. import glob
  11. import os
  12. import shutil
  13. import subprocess
  14. import sys
  15. import tempfile
  16. from importlib import import_module
  17. from pathlib import Path
  18. import pytest
  19. import numpy
  20. from numpy._utils import asunicode
  21. from numpy.f2py._backends._meson import MesonBackend
  22. from numpy.testing import IS_WASM, temppath
  23. #
  24. # Check if compilers are available at all...
  25. #
  26. def check_language(lang, code_snippet=None):
  27. if sys.platform == "win32":
  28. pytest.skip("No Fortran tests on Windows (Issue #25134)", allow_module_level=True)
  29. tmpdir = tempfile.mkdtemp()
  30. try:
  31. meson_file = os.path.join(tmpdir, "meson.build")
  32. with open(meson_file, "w") as f:
  33. f.write("project('check_compilers')\n")
  34. f.write(f"add_languages('{lang}')\n")
  35. if code_snippet:
  36. f.write(f"{lang}_compiler = meson.get_compiler('{lang}')\n")
  37. f.write(f"{lang}_code = '''{code_snippet}'''\n")
  38. f.write(
  39. f"_have_{lang}_feature ="
  40. f"{lang}_compiler.compiles({lang}_code,"
  41. f" name: '{lang} feature check')\n"
  42. )
  43. try:
  44. runmeson = subprocess.run(
  45. ["meson", "setup", "btmp"],
  46. check=False,
  47. cwd=tmpdir,
  48. capture_output=True,
  49. )
  50. except subprocess.CalledProcessError:
  51. pytest.skip("meson not present, skipping compiler dependent test", allow_module_level=True)
  52. return runmeson.returncode == 0
  53. finally:
  54. shutil.rmtree(tmpdir)
  55. fortran77_code = '''
  56. C Example Fortran 77 code
  57. PROGRAM HELLO
  58. PRINT *, 'Hello, Fortran 77!'
  59. END
  60. '''
  61. fortran90_code = '''
  62. ! Example Fortran 90 code
  63. program hello90
  64. type :: greeting
  65. character(len=20) :: text
  66. end type greeting
  67. type(greeting) :: greet
  68. greet%text = 'hello, fortran 90!'
  69. print *, greet%text
  70. end program hello90
  71. '''
  72. # Dummy class for caching relevant checks
  73. class CompilerChecker:
  74. def __init__(self):
  75. self.compilers_checked = False
  76. self.has_c = False
  77. self.has_f77 = False
  78. self.has_f90 = False
  79. def check_compilers(self):
  80. if (not self.compilers_checked) and (not sys.platform == "cygwin"):
  81. with concurrent.futures.ThreadPoolExecutor() as executor:
  82. futures = [
  83. executor.submit(check_language, "c"),
  84. executor.submit(check_language, "fortran", fortran77_code),
  85. executor.submit(check_language, "fortran", fortran90_code)
  86. ]
  87. self.has_c = futures[0].result()
  88. self.has_f77 = futures[1].result()
  89. self.has_f90 = futures[2].result()
  90. self.compilers_checked = True
  91. if not IS_WASM:
  92. checker = CompilerChecker()
  93. checker.check_compilers()
  94. def has_c_compiler():
  95. return checker.has_c
  96. def has_f77_compiler():
  97. return checker.has_f77
  98. def has_f90_compiler():
  99. return checker.has_f90
  100. def has_fortran_compiler():
  101. return (checker.has_f90 and checker.has_f77)
  102. #
  103. # Maintaining a temporary module directory
  104. #
  105. _module_dir = None
  106. _module_num = 5403
  107. if sys.platform == "cygwin":
  108. NUMPY_INSTALL_ROOT = Path(__file__).parent.parent.parent
  109. _module_list = list(NUMPY_INSTALL_ROOT.glob("**/*.dll"))
  110. def _cleanup():
  111. global _module_dir
  112. if _module_dir is not None:
  113. try:
  114. sys.path.remove(_module_dir)
  115. except ValueError:
  116. pass
  117. try:
  118. shutil.rmtree(_module_dir)
  119. except OSError:
  120. pass
  121. _module_dir = None
  122. def get_module_dir():
  123. global _module_dir
  124. if _module_dir is None:
  125. _module_dir = tempfile.mkdtemp()
  126. atexit.register(_cleanup)
  127. if _module_dir not in sys.path:
  128. sys.path.insert(0, _module_dir)
  129. return _module_dir
  130. def get_temp_module_name():
  131. # Assume single-threaded, and the module dir usable only by this thread
  132. global _module_num
  133. get_module_dir()
  134. name = "_test_ext_module_%d" % _module_num
  135. _module_num += 1
  136. if name in sys.modules:
  137. # this should not be possible, but check anyway
  138. raise RuntimeError("Temporary module name already in use.")
  139. return name
  140. def _memoize(func):
  141. memo = {}
  142. def wrapper(*a, **kw):
  143. key = repr((a, kw))
  144. if key not in memo:
  145. try:
  146. memo[key] = func(*a, **kw)
  147. except Exception as e:
  148. memo[key] = e
  149. raise
  150. ret = memo[key]
  151. if isinstance(ret, Exception):
  152. raise ret
  153. return ret
  154. wrapper.__name__ = func.__name__
  155. return wrapper
  156. #
  157. # Building modules
  158. #
  159. @_memoize
  160. def build_module(source_files, options=[], skip=[], only=[], module_name=None):
  161. """
  162. Compile and import a f2py module, built from the given files.
  163. """
  164. code = f"import sys; sys.path = {sys.path!r}; import numpy.f2py; numpy.f2py.main()"
  165. d = get_module_dir()
  166. # gh-27045 : Skip if no compilers are found
  167. if not has_fortran_compiler():
  168. pytest.skip("No Fortran compiler available")
  169. # Copy files
  170. dst_sources = []
  171. f2py_sources = []
  172. for fn in source_files:
  173. if not os.path.isfile(fn):
  174. raise RuntimeError(f"{fn} is not a file")
  175. dst = os.path.join(d, os.path.basename(fn))
  176. shutil.copyfile(fn, dst)
  177. dst_sources.append(dst)
  178. base, ext = os.path.splitext(dst)
  179. if ext in (".f90", ".f95", ".f", ".c", ".pyf"):
  180. f2py_sources.append(dst)
  181. assert f2py_sources
  182. # Prepare options
  183. if module_name is None:
  184. module_name = get_temp_module_name()
  185. gil_options = []
  186. if '--freethreading-compatible' not in options and '--no-freethreading-compatible' not in options:
  187. # default to disabling the GIL if unset in options
  188. gil_options = ['--freethreading-compatible']
  189. f2py_opts = ["-c", "-m", module_name] + options + gil_options + f2py_sources
  190. f2py_opts += ["--backend", "meson"]
  191. if skip:
  192. f2py_opts += ["skip:"] + skip
  193. if only:
  194. f2py_opts += ["only:"] + only
  195. # Build
  196. cwd = os.getcwd()
  197. try:
  198. os.chdir(d)
  199. cmd = [sys.executable, "-c", code] + f2py_opts
  200. p = subprocess.Popen(cmd,
  201. stdout=subprocess.PIPE,
  202. stderr=subprocess.STDOUT)
  203. out, err = p.communicate()
  204. if p.returncode != 0:
  205. raise RuntimeError(f"Running f2py failed: {cmd[4:]}\n{asunicode(out)}")
  206. finally:
  207. os.chdir(cwd)
  208. # Partial cleanup
  209. for fn in dst_sources:
  210. os.unlink(fn)
  211. # Rebase (Cygwin-only)
  212. if sys.platform == "cygwin":
  213. # If someone starts deleting modules after import, this will
  214. # need to change to record how big each module is, rather than
  215. # relying on rebase being able to find that from the files.
  216. _module_list.extend(
  217. glob.glob(os.path.join(d, f"{module_name:s}*"))
  218. )
  219. subprocess.check_call(
  220. ["/usr/bin/rebase", "--database", "--oblivious", "--verbose"]
  221. + _module_list
  222. )
  223. # Import
  224. return import_module(module_name)
  225. @_memoize
  226. def build_code(source_code,
  227. options=[],
  228. skip=[],
  229. only=[],
  230. suffix=None,
  231. module_name=None):
  232. """
  233. Compile and import Fortran code using f2py.
  234. """
  235. if suffix is None:
  236. suffix = ".f"
  237. with temppath(suffix=suffix) as path:
  238. with open(path, "w") as f:
  239. f.write(source_code)
  240. return build_module([path],
  241. options=options,
  242. skip=skip,
  243. only=only,
  244. module_name=module_name)
  245. #
  246. # Building with meson
  247. #
  248. class SimplifiedMesonBackend(MesonBackend):
  249. def __init__(self, *args, **kwargs):
  250. super().__init__(*args, **kwargs)
  251. def compile(self):
  252. self.write_meson_build(self.build_dir)
  253. self.run_meson(self.build_dir)
  254. def build_meson(source_files, module_name=None, **kwargs):
  255. """
  256. Build a module via Meson and import it.
  257. """
  258. # gh-27045 : Skip if no compilers are found
  259. if not has_fortran_compiler():
  260. pytest.skip("No Fortran compiler available")
  261. build_dir = get_module_dir()
  262. if module_name is None:
  263. module_name = get_temp_module_name()
  264. # Initialize the MesonBackend
  265. backend = SimplifiedMesonBackend(
  266. modulename=module_name,
  267. sources=source_files,
  268. extra_objects=kwargs.get("extra_objects", []),
  269. build_dir=build_dir,
  270. include_dirs=kwargs.get("include_dirs", []),
  271. library_dirs=kwargs.get("library_dirs", []),
  272. libraries=kwargs.get("libraries", []),
  273. define_macros=kwargs.get("define_macros", []),
  274. undef_macros=kwargs.get("undef_macros", []),
  275. f2py_flags=kwargs.get("f2py_flags", []),
  276. sysinfo_flags=kwargs.get("sysinfo_flags", []),
  277. fc_flags=kwargs.get("fc_flags", []),
  278. flib_flags=kwargs.get("flib_flags", []),
  279. setup_flags=kwargs.get("setup_flags", []),
  280. remove_build_dir=kwargs.get("remove_build_dir", False),
  281. extra_dat=kwargs.get("extra_dat", {}),
  282. )
  283. backend.compile()
  284. # Import the compiled module
  285. sys.path.insert(0, f"{build_dir}/{backend.meson_build_dir}")
  286. return import_module(module_name)
  287. #
  288. # Unittest convenience
  289. #
  290. class F2PyTest:
  291. code = None
  292. sources = None
  293. options = []
  294. skip = []
  295. only = []
  296. suffix = ".f"
  297. module = None
  298. _has_c_compiler = None
  299. _has_f77_compiler = None
  300. _has_f90_compiler = None
  301. @property
  302. def module_name(self):
  303. cls = type(self)
  304. return f'_{cls.__module__.rsplit(".", 1)[-1]}_{cls.__name__}_ext_module'
  305. @classmethod
  306. def setup_class(cls):
  307. if sys.platform == "win32":
  308. pytest.skip("Fails with MinGW64 Gfortran (Issue #9673)")
  309. F2PyTest._has_c_compiler = has_c_compiler()
  310. F2PyTest._has_f77_compiler = has_f77_compiler()
  311. F2PyTest._has_f90_compiler = has_f90_compiler()
  312. F2PyTest._has_fortran_compiler = has_fortran_compiler()
  313. def setup_method(self):
  314. if self.module is not None:
  315. return
  316. codes = self.sources or []
  317. if self.code:
  318. codes.append(self.suffix)
  319. needs_f77 = any(str(fn).endswith(".f") for fn in codes)
  320. needs_f90 = any(str(fn).endswith(".f90") for fn in codes)
  321. needs_pyf = any(str(fn).endswith(".pyf") for fn in codes)
  322. if needs_f77 and not self._has_f77_compiler:
  323. pytest.skip("No Fortran 77 compiler available")
  324. if needs_f90 and not self._has_f90_compiler:
  325. pytest.skip("No Fortran 90 compiler available")
  326. if needs_pyf and not self._has_fortran_compiler:
  327. pytest.skip("No Fortran compiler available")
  328. # Build the module
  329. if self.code is not None:
  330. self.module = build_code(
  331. self.code,
  332. options=self.options,
  333. skip=self.skip,
  334. only=self.only,
  335. suffix=self.suffix,
  336. module_name=self.module_name,
  337. )
  338. if self.sources is not None:
  339. self.module = build_module(
  340. self.sources,
  341. options=self.options,
  342. skip=self.skip,
  343. only=self.only,
  344. module_name=self.module_name,
  345. )
  346. #
  347. # Helper functions
  348. #
  349. def getpath(*a):
  350. # Package root
  351. d = Path(numpy.f2py.__file__).parent.resolve()
  352. return d.joinpath(*a)
  353. @contextlib.contextmanager
  354. def switchdir(path):
  355. curpath = Path.cwd()
  356. os.chdir(path)
  357. try:
  358. yield
  359. finally:
  360. os.chdir(curpath)