Skip to content

Commit d5f3776

Browse files
authored
Merge pull request numpy#27051 from HaoZeke/fixCompilerAvail
TST: Refactor to consistently use CompilerChecker
2 parents ade6d5e + 3f70f78 commit d5f3776

File tree

3 files changed

+142
-104
lines changed

3 files changed

+142
-104
lines changed

doc/source/f2py/f2py-testing.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@ functions will be appended to ``self.module`` data member. Thus, the child class
5050
be able to access the fortran functions specified in source file by calling
5151
``self.module.[fortran_function_name]``.
5252

53+
.. versionadded:: v2.0.0b1
54+
55+
Each of the ``f2py`` tests should run without failure if no Fortran compilers
56+
are present on the host machine. To facilitate this, the ``CompilerChecker`` is
57+
used, essentially providing a ``meson`` dependent set of utilities namely
58+
``has_{c,f77,f90,fortran}_compiler()``.
59+
60+
For the CLI tests in ``test_f2py2e``, flags which are expected to call ``meson``
61+
or otherwise depend on a compiler need to call ``compiler_check_f2pycli()``
62+
instead of ``f2pycli()``.
63+
5364
Example
5465
~~~~~~~
5566

@@ -77,4 +88,4 @@ A test can be implemented as follows::
7788

7889
We override the ``sources`` data member to provide the source file. The source files
7990
are compiled and subroutines are attached to module data member when the class object
80-
is created. The ``test_module`` function calls the subroutines and tests their results.
91+
is created. The ``test_module`` function calls the subroutines and tests their results.

numpy/f2py/tests/test_f2py2e.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@
99
from numpy.f2py.f2py2e import main as f2pycli
1010
from numpy.testing._private.utils import NOGIL_BUILD
1111

12+
#######################
13+
# F2PY Test utilities #
14+
######################
15+
16+
# Tests for CLI commands which call meson will fail if no compilers are present, these are to be skipped
17+
18+
def compiler_check_f2pycli():
19+
if not util.has_fortran_compiler():
20+
pytest.skip("CLI command needs a Fortran compiler")
21+
else:
22+
f2pycli()
23+
1224
#########################
1325
# CLI utils and classes #
1426
#########################
@@ -50,9 +62,9 @@ def get_io_paths(fname_inp, mname="untitled"):
5062
)
5163

5264

53-
##############
54-
# CLI Fixtures and Tests #
55-
#############
65+
################
66+
# CLI Fixtures #
67+
################
5668

5769

5870
@pytest.fixture(scope="session")
@@ -110,6 +122,9 @@ def f2cmap_f90(tmpdir_factory):
110122
fmap.write_text(f2cmap, encoding="ascii")
111123
return fn
112124

125+
#########
126+
# Tests #
127+
#########
113128

114129
def test_gh22819_cli(capfd, gh22819_cli, monkeypatch):
115130
"""Check that module names are handled correctly
@@ -199,8 +214,7 @@ def test_gen_pyf_no_overwrite(capfd, hello_world_f90, monkeypatch):
199214
assert "Use --overwrite-signature to overwrite" in err
200215

201216

202-
@pytest.mark.skipif((platform.system() != 'Linux') or (sys.version_info <= (3, 12)),
203-
reason='Compiler and 3.12 required')
217+
@pytest.mark.skipif(sys.version_info <= (3, 12), reason="Python 3.12 required")
204218
def test_untitled_cli(capfd, hello_world_f90, monkeypatch):
205219
"""Check that modules are named correctly
206220
@@ -209,7 +223,7 @@ def test_untitled_cli(capfd, hello_world_f90, monkeypatch):
209223
ipath = Path(hello_world_f90)
210224
monkeypatch.setattr(sys, "argv", f"f2py --backend meson -c {ipath}".split())
211225
with util.switchdir(ipath.parent):
212-
f2pycli()
226+
compiler_check_f2pycli()
213227
out, _ = capfd.readouterr()
214228
assert "untitledmodule.c" in out
215229

@@ -226,7 +240,7 @@ def test_no_py312_distutils_fcompiler(capfd, hello_world_f90, monkeypatch):
226240
sys, "argv", f"f2py {ipath} -c --fcompiler=gfortran -m {MNAME}".split()
227241
)
228242
with util.switchdir(ipath.parent):
229-
f2pycli()
243+
compiler_check_f2pycli()
230244
out, _ = capfd.readouterr()
231245
assert "--fcompiler cannot be used with meson" in out
232246
monkeypatch.setattr(
@@ -759,7 +773,7 @@ def test_no_freethreading_compatible(hello_world_f90, monkeypatch):
759773
monkeypatch.setattr(sys, "argv", f'f2py -m blah {ipath} -c --no-freethreading-compatible'.split())
760774

761775
with util.switchdir(ipath.parent):
762-
f2pycli()
776+
compiler_check_f2pycli()
763777
cmd = f"{sys.executable} -c \"import blah; blah.hi();"
764778
if NOGIL_BUILD:
765779
cmd += "import sys; assert sys._is_gil_enabled() is True\""
@@ -784,7 +798,7 @@ def test_freethreading_compatible(hello_world_f90, monkeypatch):
784798
monkeypatch.setattr(sys, "argv", f'f2py -m blah {ipath} -c --freethreading-compatible'.split())
785799

786800
with util.switchdir(ipath.parent):
787-
f2pycli()
801+
compiler_check_f2pycli()
788802
cmd = f"{sys.executable} -c \"import blah; blah.hi();"
789803
if NOGIL_BUILD:
790804
cmd += "import sys; assert sys._is_gil_enabled() is False\""

numpy/f2py/tests/util.py

Lines changed: 107 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,102 @@
2626
from importlib import import_module
2727
from numpy.f2py._backends._meson import MesonBackend
2828

29+
#
30+
# Check if compilers are available at all...
31+
#
32+
33+
def check_language(lang, code_snippet=None):
34+
if sys.platform == "win32":
35+
pytest.skip("No Fortran tests on Windows (Issue #25134)", allow_module_level=True)
36+
tmpdir = tempfile.mkdtemp()
37+
try:
38+
meson_file = os.path.join(tmpdir, "meson.build")
39+
with open(meson_file, "w") as f:
40+
f.write("project('check_compilers')\n")
41+
f.write(f"add_languages('{lang}')\n")
42+
if code_snippet:
43+
f.write(f"{lang}_compiler = meson.get_compiler('{lang}')\n")
44+
f.write(f"{lang}_code = '''{code_snippet}'''\n")
45+
f.write(
46+
f"_have_{lang}_feature ="
47+
f"{lang}_compiler.compiles({lang}_code,"
48+
f" name: '{lang} feature check')\n"
49+
)
50+
try:
51+
runmeson = subprocess.run(
52+
["meson", "setup", "btmp"],
53+
check=False,
54+
cwd=tmpdir,
55+
capture_output=True,
56+
)
57+
except subprocess.CalledProcessError:
58+
pytest.skip("meson not present, skipping compiler dependent test", allow_module_level=True)
59+
return runmeson.returncode == 0
60+
finally:
61+
shutil.rmtree(tmpdir)
62+
return False
63+
64+
65+
fortran77_code = '''
66+
C Example Fortran 77 code
67+
PROGRAM HELLO
68+
PRINT *, 'Hello, Fortran 77!'
69+
END
70+
'''
71+
72+
fortran90_code = '''
73+
! Example Fortran 90 code
74+
program hello90
75+
type :: greeting
76+
character(len=20) :: text
77+
end type greeting
78+
79+
type(greeting) :: greet
80+
greet%text = 'hello, fortran 90!'
81+
print *, greet%text
82+
end program hello90
83+
'''
84+
85+
# Dummy class for caching relevant checks
86+
class CompilerChecker:
87+
def __init__(self):
88+
self.compilers_checked = False
89+
self.has_c = False
90+
self.has_f77 = False
91+
self.has_f90 = False
92+
93+
def check_compilers(self):
94+
if (not self.compilers_checked) and (not sys.platform == "cygwin"):
95+
with concurrent.futures.ThreadPoolExecutor() as executor:
96+
futures = [
97+
executor.submit(check_language, "c"),
98+
executor.submit(check_language, "fortran", fortran77_code),
99+
executor.submit(check_language, "fortran", fortran90_code)
100+
]
101+
102+
self.has_c = futures[0].result()
103+
self.has_f77 = futures[1].result()
104+
self.has_f90 = futures[2].result()
105+
106+
self.compilers_checked = True
107+
108+
if not IS_WASM:
109+
checker = CompilerChecker()
110+
checker.check_compilers()
111+
112+
def has_c_compiler():
113+
return checker.has_c
114+
115+
def has_f77_compiler():
116+
return checker.has_f77
117+
118+
def has_f90_compiler():
119+
return checker.has_f90
120+
121+
def has_fortran_compiler():
122+
return (checker.has_f90 and checker.has_f77)
123+
124+
29125
#
30126
# Maintaining a temporary module directory
31127
#
@@ -109,6 +205,9 @@ def build_module(source_files, options=[], skip=[], only=[], module_name=None):
109205
code = f"import sys; sys.path = {sys.path!r}; import numpy.f2py; numpy.f2py.main()"
110206

111207
d = get_module_dir()
208+
# gh-27045 : Skip if no compilers are found
209+
if not has_fortran_compiler():
210+
pytest.skip("No Fortran compiler available")
112211

113212
# Copy files
114213
dst_sources = []
@@ -199,92 +298,6 @@ def build_code(source_code,
199298
module_name=module_name)
200299

201300

202-
#
203-
# Check if compilers are available at all...
204-
#
205-
206-
def check_language(lang, code_snippet=None):
207-
tmpdir = tempfile.mkdtemp()
208-
try:
209-
meson_file = os.path.join(tmpdir, "meson.build")
210-
with open(meson_file, "w") as f:
211-
f.write("project('check_compilers')\n")
212-
f.write(f"add_languages('{lang}')\n")
213-
if code_snippet:
214-
f.write(f"{lang}_compiler = meson.get_compiler('{lang}')\n")
215-
f.write(f"{lang}_code = '''{code_snippet}'''\n")
216-
f.write(
217-
f"_have_{lang}_feature ="
218-
f"{lang}_compiler.compiles({lang}_code,"
219-
f" name: '{lang} feature check')\n"
220-
)
221-
runmeson = subprocess.run(
222-
["meson", "setup", "btmp"],
223-
check=False,
224-
cwd=tmpdir,
225-
capture_output=True,
226-
)
227-
return runmeson.returncode == 0
228-
finally:
229-
shutil.rmtree(tmpdir)
230-
return False
231-
232-
fortran77_code = '''
233-
C Example Fortran 77 code
234-
PROGRAM HELLO
235-
PRINT *, 'Hello, Fortran 77!'
236-
END
237-
'''
238-
239-
fortran90_code = '''
240-
! Example Fortran 90 code
241-
program hello90
242-
type :: greeting
243-
character(len=20) :: text
244-
end type greeting
245-
246-
type(greeting) :: greet
247-
greet%text = 'hello, fortran 90!'
248-
print *, greet%text
249-
end program hello90
250-
'''
251-
252-
# Dummy class for caching relevant checks
253-
class CompilerChecker:
254-
def __init__(self):
255-
self.compilers_checked = False
256-
self.has_c = False
257-
self.has_f77 = False
258-
self.has_f90 = False
259-
260-
def check_compilers(self):
261-
if (not self.compilers_checked) and (not sys.platform == "cygwin"):
262-
with concurrent.futures.ThreadPoolExecutor() as executor:
263-
futures = [
264-
executor.submit(check_language, "c"),
265-
executor.submit(check_language, "fortran", fortran77_code),
266-
executor.submit(check_language, "fortran", fortran90_code)
267-
]
268-
269-
self.has_c = futures[0].result()
270-
self.has_f77 = futures[1].result()
271-
self.has_f90 = futures[2].result()
272-
273-
self.compilers_checked = True
274-
275-
if not IS_WASM:
276-
checker = CompilerChecker()
277-
checker.check_compilers()
278-
279-
def has_c_compiler():
280-
return checker.has_c
281-
282-
def has_f77_compiler():
283-
return checker.has_f77
284-
285-
def has_f90_compiler():
286-
return checker.has_f90
287-
288301
#
289302
# Building with meson
290303
#
@@ -303,6 +316,11 @@ def build_meson(source_files, module_name=None, **kwargs):
303316
"""
304317
Build a module via Meson and import it.
305318
"""
319+
320+
# gh-27045 : Skip if no compilers are found
321+
if not has_fortran_compiler():
322+
pytest.skip("No Fortran compiler available")
323+
306324
build_dir = get_module_dir()
307325
if module_name is None:
308326
module_name = get_temp_module_name()
@@ -327,13 +345,7 @@ def build_meson(source_files, module_name=None, **kwargs):
327345
extra_dat=kwargs.get("extra_dat", {}),
328346
)
329347

330-
# Compile the module
331-
# NOTE: Catch-all since without distutils it is hard to determine which
332-
# compiler stack is on the CI
333-
try:
334-
backend.compile()
335-
except subprocess.CalledProcessError:
336-
pytest.skip("Failed to compile module")
348+
backend.compile()
337349

338350
# Import the compiled module
339351
sys.path.insert(0, f"{build_dir}/{backend.meson_build_dir}")
@@ -369,6 +381,7 @@ def setup_class(cls):
369381
F2PyTest._has_c_compiler = has_c_compiler()
370382
F2PyTest._has_f77_compiler = has_f77_compiler()
371383
F2PyTest._has_f90_compiler = has_f90_compiler()
384+
F2PyTest._has_fortran_compiler = has_fortran_compiler()
372385

373386
def setup_method(self):
374387
if self.module is not None:
@@ -386,7 +399,7 @@ def setup_method(self):
386399
pytest.skip("No Fortran 77 compiler available")
387400
if needs_f90 and not self._has_f90_compiler:
388401
pytest.skip("No Fortran 90 compiler available")
389-
if needs_pyf and not (self._has_f90_compiler or self._has_f77_compiler):
402+
if needs_pyf and not self._has_fortran_compiler:
390403
pytest.skip("No Fortran compiler available")
391404

392405
# Build the module

0 commit comments

Comments
 (0)