diff --git a/numpy/distutils/tests/test_build_ext.py b/numpy/distutils/tests/test_build_ext.py index 372100fc06c8..55e134b2a047 100644 --- a/numpy/distutils/tests/test_build_ext.py +++ b/numpy/distutils/tests/test_build_ext.py @@ -18,7 +18,7 @@ def test_multi_fortran_libs_link(tmp_path): # We need to make sure we actually have an f77 compiler. # This is nontrivial, so we'll borrow the utilities # from f2py tests: - from numpy.f2py.tests.util import has_f77_compiler + from numpy.distutils.tests.utilities import has_f77_compiler if not has_f77_compiler(): pytest.skip('No F77 compiler found') diff --git a/numpy/distutils/tests/utilities.py b/numpy/distutils/tests/utilities.py new file mode 100644 index 000000000000..5016a83d2164 --- /dev/null +++ b/numpy/distutils/tests/utilities.py @@ -0,0 +1,90 @@ +# Kanged out of numpy.f2py.tests.util for test_build_ext +from numpy.testing import IS_WASM +import textwrap +import shutil +import tempfile +import os +import re +import subprocess +import sys + +# +# Check if compilers are available at all... +# + +_compiler_status = None + + +def _get_compiler_status(): + global _compiler_status + if _compiler_status is not None: + return _compiler_status + + _compiler_status = (False, False, False) + if IS_WASM: + # Can't run compiler from inside WASM. + return _compiler_status + + # XXX: this is really ugly. But I don't know how to invoke Distutils + # in a safer way... + code = textwrap.dedent( + f"""\ + import os + import sys + sys.path = {repr(sys.path)} + + def configuration(parent_name='',top_path=None): + global config + from numpy.distutils.misc_util import Configuration + config = Configuration('', parent_name, top_path) + return config + + from numpy.distutils.core import setup + setup(configuration=configuration) + + config_cmd = config.get_config_cmd() + have_c = config_cmd.try_compile('void foo() {{}}') + print('COMPILERS:%%d,%%d,%%d' %% (have_c, + config.have_f77c(), + config.have_f90c())) + sys.exit(99) + """ + ) + code = code % dict(syspath=repr(sys.path)) + + tmpdir = tempfile.mkdtemp() + try: + script = os.path.join(tmpdir, "setup.py") + + with open(script, "w") as f: + f.write(code) + + cmd = [sys.executable, "setup.py", "config"] + p = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=tmpdir + ) + out, err = p.communicate() + finally: + shutil.rmtree(tmpdir) + + m = re.search(rb"COMPILERS:(\d+),(\d+),(\d+)", out) + if m: + _compiler_status = ( + bool(int(m.group(1))), + bool(int(m.group(2))), + bool(int(m.group(3))), + ) + # Finished + return _compiler_status + + +def has_c_compiler(): + return _get_compiler_status()[0] + + +def has_f77_compiler(): + return _get_compiler_status()[1] + + +def has_f90_compiler(): + return _get_compiler_status()[2] diff --git a/numpy/f2py/_backends/_meson.py b/numpy/f2py/_backends/_meson.py index 59cc77146f6f..8691af6ee517 100644 --- a/numpy/f2py/_backends/_meson.py +++ b/numpy/f2py/_backends/_meson.py @@ -91,13 +91,6 @@ def _move_exec_to_root(self, build_dir: Path): for path_object in path_objects: shutil.move(path_object, Path.cwd()) - def _get_build_command(self): - return [ - "meson", - "setup", - self.meson_build_dir, - ] - def write_meson_build(self, build_dir: Path) -> None: """Writes the meson build file at specified location""" meson_template = MesonTemplate( @@ -115,19 +108,14 @@ def write_meson_build(self, build_dir: Path) -> None: meson_build_file.write_text(src) return meson_build_file + def _run_subprocess_command(self, command, cwd): + subprocess.run(command, cwd=cwd, check=True) + def run_meson(self, build_dir: Path): - completed_process = subprocess.run(self._get_build_command(), cwd=build_dir) - if completed_process.returncode != 0: - raise subprocess.CalledProcessError( - completed_process.returncode, completed_process.args - ) - completed_process = subprocess.run( - ["meson", "compile", "-C", self.meson_build_dir], cwd=build_dir - ) - if completed_process.returncode != 0: - raise subprocess.CalledProcessError( - completed_process.returncode, completed_process.args - ) + setup_command = ["meson", "setup", self.meson_build_dir] + self._run_subprocess_command(setup_command, build_dir) + compile_command = ["meson", "compile", "-C", self.meson_build_dir] + self._run_subprocess_command(compile_command, build_dir) def compile(self) -> None: self.sources = _prepare_sources(self.modulename, self.sources, self.build_dir) diff --git a/numpy/f2py/tests/test_abstract_interface.py b/numpy/f2py/tests/test_abstract_interface.py index 42902913ed75..2c6555aecea1 100644 --- a/numpy/f2py/tests/test_abstract_interface.py +++ b/numpy/f2py/tests/test_abstract_interface.py @@ -7,6 +7,7 @@ @pytest.mark.skipif(IS_WASM, reason="Cannot start subprocess") +@pytest.mark.slow class TestAbstractInterface(util.F2PyTest): sources = [util.getpath("tests", "src", "abstract_interface", "foo.f90")] diff --git a/numpy/f2py/tests/test_array_from_pyobj.py b/numpy/f2py/tests/test_array_from_pyobj.py index 9aad7b6ca708..4e41899138f4 100644 --- a/numpy/f2py/tests/test_array_from_pyobj.py +++ b/numpy/f2py/tests/test_array_from_pyobj.py @@ -3,6 +3,7 @@ import copy import platform import pytest +from pathlib import Path import numpy as np @@ -19,6 +20,10 @@ ) +def get_testdir(): + testroot = Path(__file__).resolve().parent / "src" + return testroot / "array_from_pyobj" + def setup_module(): """ Build the required testing extension module @@ -26,24 +31,11 @@ def setup_module(): """ global wrap - # Check compiler availability first - if not util.has_c_compiler(): - pytest.skip("No C compiler available") - if wrap is None: - config_code = """ - config.add_extension('test_array_from_pyobj_ext', - sources=['wrapmodule.c', 'fortranobject.c'], - define_macros=[]) - """ - d = os.path.dirname(__file__) src = [ - util.getpath("tests", "src", "array_from_pyobj", "wrapmodule.c"), - util.getpath("src", "fortranobject.c"), - util.getpath("src", "fortranobject.h"), + get_testdir() / "wrapmodule.c", ] - wrap = util.build_module_distutils(src, config_code, - "test_array_from_pyobj_ext") + wrap = util.build_meson(src, module_name = "test_array_from_pyobj_ext") def flags_info(arr): diff --git a/numpy/f2py/tests/test_block_docstring.py b/numpy/f2py/tests/test_block_docstring.py index e0eacc0329c5..16b5559e8e42 100644 --- a/numpy/f2py/tests/test_block_docstring.py +++ b/numpy/f2py/tests/test_block_docstring.py @@ -5,6 +5,7 @@ from numpy.testing import IS_PYPY +@pytest.mark.slow class TestBlockDocString(util.F2PyTest): sources = [util.getpath("tests", "src", "block_docstring", "foo.f")] diff --git a/numpy/f2py/tests/test_callback.py b/numpy/f2py/tests/test_callback.py index 5b6c294d33fc..8bd6175a3eb9 100644 --- a/numpy/f2py/tests/test_callback.py +++ b/numpy/f2py/tests/test_callback.py @@ -15,6 +15,7 @@ class TestF77Callback(util.F2PyTest): sources = [util.getpath("tests", "src", "callback", "foo.f")] @pytest.mark.parametrize("name", "t,t2".split(",")) + @pytest.mark.slow def test_all(self, name): self.check_function(name) @@ -205,6 +206,7 @@ class TestF77CallbackPythonTLS(TestF77Callback): class TestF90Callback(util.F2PyTest): sources = [util.getpath("tests", "src", "callback", "gh17797.f90")] + @pytest.mark.slow def test_gh17797(self): def incr(x): return x + 123 @@ -222,6 +224,7 @@ class TestGH18335(util.F2PyTest): """ sources = [util.getpath("tests", "src", "callback", "gh18335.f90")] + @pytest.mark.slow def test_gh18335(self): def foo(x): x[0] += 1 @@ -235,7 +238,7 @@ class TestGH25211(util.F2PyTest): util.getpath("tests", "src", "callback", "gh25211.pyf")] module_name = "callback2" - def test_gh18335(self): + def test_gh25211(self): def bar(x): return x*x diff --git a/numpy/f2py/tests/test_character.py b/numpy/f2py/tests/test_character.py index c928e38741a1..078f5fcd3925 100644 --- a/numpy/f2py/tests/test_character.py +++ b/numpy/f2py/tests/test_character.py @@ -5,6 +5,7 @@ from numpy.f2py.tests import util +@pytest.mark.slow class TestCharacterString(util.F2PyTest): # options = ['--debug-capi', '--build-dir', '/tmp/test-build-f2py'] suffix = '.f90' @@ -512,6 +513,7 @@ class TestMiscCharacter(util.F2PyTest): end subroutine {fprefix}_character_bc_old """) + @pytest.mark.slow def test_gh18684(self): # Test character(len=5) and character*5 usages f = getattr(self.module, self.fprefix + '_gh18684') @@ -596,6 +598,7 @@ class TestStringAssumedLength(util.F2PyTest): def test_gh24008(self): self.module.greet("joe", "bob") +@pytest.mark.slow class TestStringOptionalInOut(util.F2PyTest): sources = [util.getpath("tests", "src", "string", "gh24662.f90")] diff --git a/numpy/f2py/tests/test_common.py b/numpy/f2py/tests/test_common.py index 68c1b3b31c5d..09bd6147f0f3 100644 --- a/numpy/f2py/tests/test_common.py +++ b/numpy/f2py/tests/test_common.py @@ -1,16 +1,11 @@ -import os -import sys import pytest - import numpy as np from . import util - +@pytest.mark.slow class TestCommonBlock(util.F2PyTest): sources = [util.getpath("tests", "src", "common", "block.f")] - @pytest.mark.skipif(sys.platform == "win32", - reason="Fails with MinGW64 Gfortran (Issue #9673)") def test_common_block(self): self.module.initcb() assert self.module.block.long_bn == np.array(1.0, dtype=np.float64) @@ -21,7 +16,5 @@ def test_common_block(self): class TestCommonWithUse(util.F2PyTest): sources = [util.getpath("tests", "src", "common", "gh19161.f90")] - @pytest.mark.skipif(sys.platform == "win32", - reason="Fails with MinGW64 Gfortran (Issue #9673)") def test_common_gh19161(self): assert self.module.data.x == 0 diff --git a/numpy/f2py/tests/test_crackfortran.py b/numpy/f2py/tests/test_crackfortran.py index c8d9ddb88460..164194b27452 100644 --- a/numpy/f2py/tests/test_crackfortran.py +++ b/numpy/f2py/tests/test_crackfortran.py @@ -210,6 +210,7 @@ class TestDimSpec(util.F2PyTest): ) @pytest.mark.parametrize("dimspec", all_dimspecs) + @pytest.mark.slow def test_array_size(self, dimspec): count = self.all_dimspecs.index(dimspec) @@ -276,6 +277,7 @@ def test_input_encoding(self, tmp_path, encoding): assert mod[0]['name'] == 'foo' +@pytest.mark.slow class TestUnicodeComment(util.F2PyTest): sources = [util.getpath("tests", "src", "crackfortran", "unicode_comment.f90")] @@ -327,6 +329,7 @@ def test_nameargspattern_backtracking(self, adversary): class TestFunctionReturn(util.F2PyTest): sources = [util.getpath("tests", "src", "crackfortran", "gh23598.f90")] + @pytest.mark.slow def test_function_rettype(self): # gh-23598 assert self.module.intproduct(3, 4) == 12 diff --git a/numpy/f2py/tests/test_data.py b/numpy/f2py/tests/test_data.py index 4e5604c006b1..5af5c40447d3 100644 --- a/numpy/f2py/tests/test_data.py +++ b/numpy/f2py/tests/test_data.py @@ -10,6 +10,7 @@ class TestData(util.F2PyTest): sources = [util.getpath("tests", "src", "crackfortran", "data_stmts.f90")] # For gh-23276 + @pytest.mark.slow def test_data_stmts(self): assert self.module.cmplxdat.i == 2 assert self.module.cmplxdat.j == 3 diff --git a/numpy/f2py/tests/test_docs.py b/numpy/f2py/tests/test_docs.py index 6631dd82c9c7..6761ca53d4bc 100644 --- a/numpy/f2py/tests/test_docs.py +++ b/numpy/f2py/tests/test_docs.py @@ -3,27 +3,23 @@ import numpy as np from numpy.testing import assert_array_equal, assert_equal from . import util - +from pathlib import Path def get_docdir(): - # assuming that documentation tests are run from a source - # directory - return os.path.abspath(os.path.join( - os.path.dirname(__file__), - '..', '..', '..', - 'doc', 'source', 'f2py', 'code')) - + # Assumes that spin is used to run tests + nproot = Path(__file__).resolve().parents[8] + return nproot / "doc" / "source" / "f2py" / "code" pytestmark = pytest.mark.skipif( - not os.path.isdir(get_docdir()), - reason=('Could not find f2py documentation sources' - f' ({get_docdir()} does not exists)')) - - -def _path(*a): - return os.path.join(*((get_docdir(),) + a)) + not get_docdir().is_dir(), + reason=f"Could not find f2py documentation sources" + f"({get_docdir()} does not exist)", +) +def _path(*args): + return get_docdir().joinpath(*args) +@pytest.mark.slow class TestDocAdvanced(util.F2PyTest): # options = ['--debug-capi', '--build-dir', '/tmp/build-f2py'] sources = [_path('asterisk1.f90'), _path('asterisk2.f90'), @@ -37,7 +33,7 @@ def test_asterisk2(self): foo = getattr(self.module, 'foo2') assert_equal(foo(2), b'12') assert_equal(foo(12), b'123456789A12') - assert_equal(foo(24), b'123456789A123456789B') + assert_equal(foo(20), b'123456789A123456789B') def test_ftype(self): ftype = self.module diff --git a/numpy/f2py/tests/test_f2cmap.py b/numpy/f2py/tests/test_f2cmap.py index d2967e4f73d7..6596ada33a54 100644 --- a/numpy/f2py/tests/test_f2cmap.py +++ b/numpy/f2py/tests/test_f2cmap.py @@ -8,7 +8,7 @@ class TestF2Cmap(util.F2PyTest): ] # gh-15095 - def test_long_long_map(self): + def test_gh15095(self): inp = np.ones(3) out = self.module.func1(inp) exp_out = 3 diff --git a/numpy/f2py/tests/test_isoc.py b/numpy/f2py/tests/test_isoc.py index 7e189bd7b830..808dadc7d7e9 100644 --- a/numpy/f2py/tests/test_isoc.py +++ b/numpy/f2py/tests/test_isoc.py @@ -1,5 +1,6 @@ from . import util import numpy as np +import pytest class TestISOC(util.F2PyTest): sources = [ @@ -7,6 +8,7 @@ class TestISOC(util.F2PyTest): ] # gh-24553 + @pytest.mark.slow def test_c_double(self): out = self.module.coddity.c_add(1, 2) exp_out = 3 diff --git a/numpy/f2py/tests/test_kind.py b/numpy/f2py/tests/test_kind.py index 69b85aaad21b..c8cc57ff21c9 100644 --- a/numpy/f2py/tests/test_kind.py +++ b/numpy/f2py/tests/test_kind.py @@ -1,3 +1,4 @@ +import sys import os import pytest import platform @@ -12,6 +13,8 @@ class TestKind(util.F2PyTest): sources = [util.getpath("tests", "src", "kind", "foo.f90")] + @pytest.mark.skipif(sys.maxsize < 2 ** 31 + 1, + reason="Fails for 32 bit machines") def test_int(self): """Test `int` kind_func for integers up to 10**40.""" selectedintkind = self.module.selectedintkind diff --git a/numpy/f2py/tests/test_mixed.py b/numpy/f2py/tests/test_mixed.py index 80653b7d2d77..49d0ba20c29a 100644 --- a/numpy/f2py/tests/test_mixed.py +++ b/numpy/f2py/tests/test_mixed.py @@ -13,6 +13,7 @@ class TestMixed(util.F2PyTest): util.getpath("tests", "src", "mixed", "foo_free.f90"), ] + @pytest.mark.slow def test_all(self): assert self.module.bar11() == 11 assert self.module.foo_fixed.bar12() == 12 diff --git a/numpy/f2py/tests/test_module_doc.py b/numpy/f2py/tests/test_module_doc.py index 28822d405cc0..a7132bf46bc3 100644 --- a/numpy/f2py/tests/test_module_doc.py +++ b/numpy/f2py/tests/test_module_doc.py @@ -7,6 +7,7 @@ from numpy.testing import IS_PYPY +@pytest.mark.slow class TestModuleDocString(util.F2PyTest): sources = [ util.getpath("tests", "src", "module_data", diff --git a/numpy/f2py/tests/test_quoted_character.py b/numpy/f2py/tests/test_quoted_character.py index 82671cd8e72f..85e83a781e7b 100644 --- a/numpy/f2py/tests/test_quoted_character.py +++ b/numpy/f2py/tests/test_quoted_character.py @@ -12,5 +12,6 @@ class TestQuotedCharacter(util.F2PyTest): @pytest.mark.skipif(sys.platform == "win32", reason="Fails with MinGW64 Gfortran (Issue #9673)") + @pytest.mark.slow def test_quoted_character(self): assert self.module.foo() == (b"'", b'"', b";", b"!", b"(", b")") diff --git a/numpy/f2py/tests/test_return_character.py b/numpy/f2py/tests/test_return_character.py index 36c1f10f4191..078d445a6df6 100644 --- a/numpy/f2py/tests/test_return_character.py +++ b/numpy/f2py/tests/test_return_character.py @@ -7,6 +7,7 @@ IS_S390X = platform.machine() == "s390x" +@pytest.mark.slow class TestReturnCharacter(util.F2PyTest): def check_function(self, t, tname): if tname in ["t0", "t1", "s0", "s1"]: diff --git a/numpy/f2py/tests/test_return_complex.py b/numpy/f2py/tests/test_return_complex.py index e913a75a09f0..17811f5d98f9 100644 --- a/numpy/f2py/tests/test_return_complex.py +++ b/numpy/f2py/tests/test_return_complex.py @@ -4,6 +4,7 @@ from . import util +@pytest.mark.slow class TestReturnComplex(util.F2PyTest): def check_function(self, t, tname): if tname in ["t0", "t8", "s0", "s8"]: diff --git a/numpy/f2py/tests/test_return_integer.py b/numpy/f2py/tests/test_return_integer.py index 3b2f42e2bff6..428afec4a0ef 100644 --- a/numpy/f2py/tests/test_return_integer.py +++ b/numpy/f2py/tests/test_return_integer.py @@ -4,6 +4,7 @@ from . import util +@pytest.mark.slow class TestReturnInteger(util.F2PyTest): def check_function(self, t, tname): assert t(123) == 123 diff --git a/numpy/f2py/tests/test_return_real.py b/numpy/f2py/tests/test_return_real.py index a15d6475a950..d9b316dcc45d 100644 --- a/numpy/f2py/tests/test_return_real.py +++ b/numpy/f2py/tests/test_return_real.py @@ -6,6 +6,7 @@ from . import util +@pytest.mark.slow class TestReturnReal(util.F2PyTest): def check_function(self, t, tname): if tname in ["t0", "t4", "s0", "s4"]: diff --git a/numpy/f2py/tests/test_semicolon_split.py b/numpy/f2py/tests/test_semicolon_split.py index 6d499046c1a5..ab9c093dbb82 100644 --- a/numpy/f2py/tests/test_semicolon_split.py +++ b/numpy/f2py/tests/test_semicolon_split.py @@ -47,6 +47,7 @@ def test_multiline(self): np.dtype(np.intp).itemsize < 8, reason="32-bit builds are buggy" ) +@pytest.mark.slow class TestCallstatement(util.F2PyTest): suffix = ".pyf" module_name = "callstatement" diff --git a/numpy/f2py/tests/test_value_attrspec.py b/numpy/f2py/tests/test_value_attrspec.py index 83aaf6c9161e..3855a6273288 100644 --- a/numpy/f2py/tests/test_value_attrspec.py +++ b/numpy/f2py/tests/test_value_attrspec.py @@ -7,7 +7,8 @@ class TestValueAttr(util.F2PyTest): sources = [util.getpath("tests", "src", "value_attrspec", "gh21665.f90")] # gh-21665 - def test_long_long_map(self): + @pytest.mark.slow + def test_gh21665(self): inp = 2 out = self.module.fortfuncs.square(inp) exp_out = 4 diff --git a/numpy/f2py/tests/util.py b/numpy/f2py/tests/util.py index 75b257cdb825..6d6e82550c26 100644 --- a/numpy/f2py/tests/util.py +++ b/numpy/f2py/tests/util.py @@ -18,11 +18,13 @@ import pytest import contextlib import numpy +import concurrent.futures from pathlib import Path from numpy._utils import asunicode from numpy.testing import temppath, IS_WASM from importlib import import_module +from numpy.f2py._backends._meson import MesonBackend # # Maintaining a temporary module directory @@ -128,6 +130,7 @@ def build_module(source_files, options=[], skip=[], only=[], module_name=None): if module_name is None: module_name = get_temp_module_name() f2py_opts = ["-c", "-m", module_name] + options + f2py_sources + f2py_opts += ["--backend", "meson"] if skip: f2py_opts += ["skip:"] + skip if only: @@ -165,8 +168,6 @@ def build_module(source_files, options=[], skip=[], only=[], module_name=None): + _module_list ) - - # Import return import_module(module_name) @@ -198,150 +199,144 @@ def build_code(source_code, # Check if compilers are available at all... # -_compiler_status = None - - -def _get_compiler_status(): - global _compiler_status - if _compiler_status is not None: - return _compiler_status - - _compiler_status = (False, False, False) - if IS_WASM: - # Can't run compiler from inside WASM. - return _compiler_status - - # XXX: this is really ugly. But I don't know how to invoke Distutils - # in a safer way... - code = textwrap.dedent(f"""\ - import os - import sys - sys.path = {repr(sys.path)} - - def configuration(parent_name='',top_path=None): - global config - from numpy.distutils.misc_util import Configuration - config = Configuration('', parent_name, top_path) - return config - - from numpy.distutils.core import setup - setup(configuration=configuration) - - config_cmd = config.get_config_cmd() - have_c = config_cmd.try_compile('void foo() {{}}') - print('COMPILERS:%%d,%%d,%%d' %% (have_c, - config.have_f77c(), - config.have_f90c())) - sys.exit(99) - """) - code = code % dict(syspath=repr(sys.path)) - +def check_language(lang, code_snippet=None): tmpdir = tempfile.mkdtemp() try: - script = os.path.join(tmpdir, "setup.py") - - with open(script, "w") as f: - f.write(code) - - cmd = [sys.executable, "setup.py", "config"] - p = subprocess.Popen(cmd, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - cwd=tmpdir) - out, err = p.communicate() + meson_file = os.path.join(tmpdir, "meson.build") + with open(meson_file, "w") as f: + f.write("project('check_compilers')\n") + f.write(f"add_languages('{lang}')\n") + if code_snippet: + f.write(f"{lang}_compiler = meson.get_compiler('{lang}')\n") + f.write(f"{lang}_code = '''{code_snippet}'''\n") + f.write( + f"_have_{lang}_feature =" + f"{lang}_compiler.compiles({lang}_code," + f" name: '{lang} feature check')\n" + ) + runmeson = subprocess.run( + ["meson", "setup", "btmp"], + check=False, + cwd=tmpdir, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if runmeson.returncode == 0: + return True + else: + return False finally: shutil.rmtree(tmpdir) - - m = re.search(br"COMPILERS:(\d+),(\d+),(\d+)", out) - if m: - _compiler_status = ( - bool(int(m.group(1))), - bool(int(m.group(2))), - bool(int(m.group(3))), - ) - # Finished - return _compiler_status - + return False + +fortran77_code = ''' +C Example Fortran 77 code + PROGRAM HELLO + PRINT *, 'Hello, Fortran 77!' + END +''' + +fortran90_code = ''' +! Example Fortran 90 code +program hello90 + type :: greeting + character(len=20) :: text + end type greeting + + type(greeting) :: greet + greet%text = 'hello, fortran 90!' + print *, greet%text +end program hello90 +''' + +# Dummy class for caching relevant checks +class CompilerChecker: + def __init__(self): + self.compilers_checked = False + self.has_c = False + self.has_f77 = False + self.has_f90 = False + + def check_compilers(self): + if (not self.compilers_checked) and (not sys.platform == "cygwin"): + with concurrent.futures.ThreadPoolExecutor() as executor: + futures = [ + executor.submit(check_language, "c"), + executor.submit(check_language, "fortran", fortran77_code), + executor.submit(check_language, "fortran", fortran90_code) + ] + + self.has_c = futures[0].result() + self.has_f77 = futures[1].result() + self.has_f90 = futures[2].result() + + self.compilers_checked = True + +checker = CompilerChecker() +checker.check_compilers() def has_c_compiler(): - return _get_compiler_status()[0] - + return checker.has_c def has_f77_compiler(): - return _get_compiler_status()[1] - + return checker.has_f77 def has_f90_compiler(): - return _get_compiler_status()[2] - + return checker.has_f90 # -# Building with distutils +# Building with meson # -@_memoize -def build_module_distutils(source_files, config_code, module_name, **kw): - """ - Build a module via distutils and import it. +class SimplifiedMesonBackend(MesonBackend): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) - """ - d = get_module_dir() + def compile(self): + self.write_meson_build(self.build_dir) + self.run_meson(self.build_dir) - # Copy files - dst_sources = [] - for fn in source_files: - if not os.path.isfile(fn): - raise RuntimeError("%s is not a file" % fn) - dst = os.path.join(d, os.path.basename(fn)) - shutil.copyfile(fn, dst) - dst_sources.append(dst) - - # Build script - config_code = textwrap.dedent(config_code).replace("\n", "\n ") - - code = fr""" -import os -import sys -sys.path = {repr(sys.path)} -def configuration(parent_name='',top_path=None): - from numpy.distutils.misc_util import Configuration - config = Configuration('', parent_name, top_path) - {config_code} - return config - -if __name__ == "__main__": - from numpy.distutils.core import setup - setup(configuration=configuration) +def build_meson(source_files, module_name=None, **kwargs): + """ + Build a module via Meson and import it. """ - script = os.path.join(d, get_temp_module_name() + ".py") - dst_sources.append(script) - with open(script, "wb") as f: - f.write(code.encode('latin1')) + build_dir = get_module_dir() + if module_name is None: + module_name = get_temp_module_name() - # Build - cwd = os.getcwd() + # Initialize the MesonBackend + backend = SimplifiedMesonBackend( + modulename=module_name, + sources=source_files, + extra_objects=kwargs.get("extra_objects", []), + build_dir=build_dir, + include_dirs=kwargs.get("include_dirs", []), + library_dirs=kwargs.get("library_dirs", []), + libraries=kwargs.get("libraries", []), + define_macros=kwargs.get("define_macros", []), + undef_macros=kwargs.get("undef_macros", []), + f2py_flags=kwargs.get("f2py_flags", []), + sysinfo_flags=kwargs.get("sysinfo_flags", []), + fc_flags=kwargs.get("fc_flags", []), + flib_flags=kwargs.get("flib_flags", []), + setup_flags=kwargs.get("setup_flags", []), + remove_build_dir=kwargs.get("remove_build_dir", False), + extra_dat=kwargs.get("extra_dat", {}), + ) + + # Compile the module + # NOTE: Catch-all since without distutils it is hard to determine which + # compiler stack is on the CI try: - os.chdir(d) - cmd = [sys.executable, script, "build_ext", "-i"] - p = subprocess.Popen(cmd, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - out, err = p.communicate() - if p.returncode != 0: - raise RuntimeError("Running distutils build failed: %s\n%s" % - (cmd[4:], asstr(out))) - finally: - os.chdir(cwd) - - # Partial cleanup - for fn in dst_sources: - os.unlink(fn) + backend.compile() + except: + pytest.skip("Failed to compile module") - # Import - __import__(module_name) - return sys.modules[module_name] + # Import the compiled module + sys.path.insert(0, f"{build_dir}/{backend.meson_build_dir}") + return import_module(module_name) # @@ -357,44 +352,40 @@ class F2PyTest: only = [] suffix = ".f" module = None + _has_c_compiler = None + _has_f77_compiler = None + _has_f90_compiler = None @property def module_name(self): cls = type(self) return f'_{cls.__module__.rsplit(".",1)[-1]}_{cls.__name__}_ext_module' - def setup_method(self): + @classmethod + def setup_class(cls): if sys.platform == "win32": pytest.skip("Fails with MinGW64 Gfortran (Issue #9673)") + F2PyTest._has_c_compiler = has_c_compiler() + F2PyTest._has_f77_compiler = has_f77_compiler() + F2PyTest._has_f90_compiler = has_f90_compiler() + def setup_method(self): if self.module is not None: return - # Check compiler availability first - if not has_c_compiler(): - pytest.skip("No C compiler available") - - codes = [] - if self.sources: - codes.extend(self.sources) - if self.code is not None: + codes = self.sources if self.sources else [] + if self.code: codes.append(self.suffix) - needs_f77 = False - needs_f90 = False - needs_pyf = False - for fn in codes: - if str(fn).endswith(".f"): - needs_f77 = True - elif str(fn).endswith(".f90"): - needs_f90 = True - elif str(fn).endswith(".pyf"): - needs_pyf = True - if needs_f77 and not has_f77_compiler(): + needs_f77 = any(str(fn).endswith(".f") for fn in codes) + needs_f90 = any(str(fn).endswith(".f90") for fn in codes) + needs_pyf = any(str(fn).endswith(".pyf") for fn in codes) + + if needs_f77 and not self._has_f77_compiler: pytest.skip("No Fortran 77 compiler available") - if needs_f90 and not has_f90_compiler(): + if needs_f90 and not self._has_f90_compiler: pytest.skip("No Fortran 90 compiler available") - if needs_pyf and not (has_f90_compiler() or has_f77_compiler()): + if needs_pyf and not (self._has_f90_compiler or self._has_f77_compiler): pytest.skip("No Fortran compiler available") # Build the module