diff --git a/numpy/distutils/command/build_src.py b/numpy/distutils/command/build_src.py
index 5581011f6f22..9b42e137543c 100644
--- a/numpy/distutils/command/build_src.py
+++ b/numpy/distutils/command/build_src.py
@@ -539,8 +539,8 @@ def f2py_sources(self, sources, extension):
if (self.force or newer_group(depends, target_file, 'newer')) \
and not skip_f2py:
log.info("f2py: %s" % (source))
- import numpy.f2py
- numpy.f2py.run_main(f2py_options
+ from numpy.f2py import f2py2e
+ f2py2e.run_main(f2py_options
+ ['--build-dir', target_dir, source])
else:
log.debug(" skipping '%s' f2py interface (up-to-date)" % (source))
@@ -558,8 +558,8 @@ def f2py_sources(self, sources, extension):
and not skip_f2py:
log.info("f2py:> %s" % (target_file))
self.mkpath(target_dir)
- import numpy.f2py
- numpy.f2py.run_main(f2py_options + ['--lower',
+ from numpy.f2py import f2py2e
+ f2py2e.run_main(f2py_options + ['--lower',
'--build-dir', target_dir]+\
['-m', ext_name]+f_sources)
else:
diff --git a/numpy/f2py/__init__.py b/numpy/f2py/__init__.py
index 84192f7386d1..9ce3e341b6b1 100644
--- a/numpy/f2py/__init__.py
+++ b/numpy/f2py/__init__.py
@@ -2,17 +2,14 @@
"""Fortran to Python Interface Generator.
"""
-__all__ = ['run_main', 'compile', 'get_include']
+__all__ = ['main', 'compile', 'get_include']
import sys
import subprocess
import os
-from . import f2py2e
from . import diagnose
-
-run_main = f2py2e.run_main
-main = f2py2e.main
+from numpy.f2py.f2pyarg import main
def compile(source,
@@ -102,7 +99,7 @@ def compile(source,
c = [sys.executable,
'-c',
- 'import numpy.f2py as f2py2e;f2py2e.main()'] + args
+ 'from numpy.f2py import main;main()'] + args
try:
cp = subprocess.run(c, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
diff --git a/numpy/f2py/__main__.py b/numpy/f2py/__main__.py
index 936a753a2796..1bf2d8da6979 100644
--- a/numpy/f2py/__main__.py
+++ b/numpy/f2py/__main__.py
@@ -1,5 +1,5 @@
# See:
# https://web.archive.org/web/20140822061353/http://cens.ioc.ee/projects/f2py2e
-from numpy.f2py.f2py2e import main
+from numpy.f2py.f2pyarg import main
main()
diff --git a/numpy/f2py/backends/__init__.py b/numpy/f2py/backends/__init__.py
new file mode 100644
index 000000000000..4ef3593b33c9
--- /dev/null
+++ b/numpy/f2py/backends/__init__.py
@@ -0,0 +1,6 @@
+from .backend import Backend
+from .meson_backend import MesonBackend
+
+backends = {
+ 'meson': MesonBackend
+}
\ No newline at end of file
diff --git a/numpy/f2py/backends/backend.py b/numpy/f2py/backends/backend.py
new file mode 100644
index 000000000000..0608bcfce207
--- /dev/null
+++ b/numpy/f2py/backends/backend.py
@@ -0,0 +1,110 @@
+from __future__ import annotations
+
+from pathlib import Path
+from abc import ABC, abstractmethod
+
+import numpy
+import numpy.f2py as f2py
+
+class Backend(ABC):
+ """
+ Superclass for backend compilation plugins to extend.
+
+ """
+
+ def __init__(self, module_name, fortran_compiler: str, c_compiler: str,
+ f77exec: Path, f90exec: Path, f77_flags: list[str],
+ f90_flags: list[str], include_paths: list[Path],
+ include_dirs: list[Path], external_resources: list[str],
+ linker_libpath: list[Path], linker_libname: list[str],
+ define_macros: list[tuple[str, str]], undef_macros: list[str],
+ debug: bool, opt_flags: list[str], arch_flags: list[str],
+ no_opt: bool, no_arch: bool) -> None:
+ """
+ The class is initialized with f2py compile options.
+ The parameters are mappings of f2py compilation flags.
+
+ Parameters
+ ----------
+ module_name : str
+ The name of the module to be compiled. (-m)
+ fortran_compiler : str
+ Name of the Fortran compiler to use. (--fcompiler)
+ c_compiler : str
+ Name of the C compiler to use. (--ccompiler)
+ f77exec : Pathlike
+ Path to the fortran compiler for Fortran 77 files (--f77exec)
+ f90exec : Pathlike
+ Path to the fortran compiler for Fortran 90 and above files (--f90exec)
+ f77_flags : list
+ List of flags to pass to the fortran compiler for Fortran 77 files (--f77flags)
+ f90_flags : list
+ List of flags to pass to the fortran compiler for Fortran 90 and above files (--f90flags)
+ include_paths : list
+ Search include files from given directories (--include-paths)
+ include_dirs : list
+ Append directory
to the list of directories searched for include files. (-I)
+ external_resources : list
+ Link the extension module with (--link-)
+ linker_libname : list
+ Use the library when linking. (-l)
+ define_macros : list
+ Define to if present else define to true (-D)
+ undef_macros : list
+ Undefine (-U)
+ linker_libpath : list
+ Add directory to the list of directories to be searched for `-l`. (-L)
+ opt_flags : list
+ Optimization flags to pass to the compiler. (--opt)
+ arch_flags : list
+ Architectire specific flags to pass to the compiler (--arch)
+ no_opt : bool
+ Disable optimization. (--no-opt)
+ no_arch : bool
+ Disable architecture specific optimizations. (--no-arch)
+ debug : bool
+ Enable debugging. (--debug)
+
+ """
+ self.module_name = module_name
+ self.fortran_compiler = fortran_compiler
+ self.c_compiler = c_compiler
+ self.f77exec = f77exec
+ self.f90exec = f90exec
+ self.f77_flags = f77_flags
+ self.f90_flags = f90_flags
+ self.include_paths = include_paths
+ self.include_dirs = include_dirs
+ self.external_resources = external_resources
+ self.linker_libpath = linker_libpath
+ self.linker_libname = linker_libname
+ self.define_macros = define_macros
+ self.undef_macros = undef_macros
+ self.debug = debug
+ self.opt_flags = opt_flags
+ self.arch_flags = arch_flags
+ self.no_opt = no_opt
+ self.no_arch = no_arch
+
+ def numpy_install_path(self) -> Path:
+ """
+ Returns the install path for numpy.
+ """
+ return Path(numpy.__file__).parent
+
+ def numpy_get_include(self) -> Path:
+ """
+ Returns the include paths for numpy.
+ """
+ return Path(numpy.get_include())
+
+ def f2py_get_include(self) -> Path:
+ """
+ Returns the include paths for f2py.
+ """
+ return Path(f2py.get_include())
+
+ @abstractmethod
+ def compile(self, fortran_sources: Path, c_wrapper: Path, build_dir: Path) -> None:
+ """Compile the wrapper."""
+ pass
\ No newline at end of file
diff --git a/numpy/f2py/backends/meson_backend.py b/numpy/f2py/backends/meson_backend.py
new file mode 100644
index 000000000000..f23798c97cb2
--- /dev/null
+++ b/numpy/f2py/backends/meson_backend.py
@@ -0,0 +1,183 @@
+from __future__ import annotations
+
+import os
+import errno
+import shutil
+import subprocess
+from pathlib import Path
+
+from .backend import Backend
+from string import Template
+
+class MesonTemplate:
+ """Template meson build file generation class."""
+ def __init__(self, module_name: str, numpy_install_path, numpy_get_include: Path, f2py_get_include: Path, wrappers: list[Path], fortran_sources: list[Path], dependencies: list[str], include_path: list[Path], optimization_flags: list[str], architecture_flags: list[str], f77_flags: list[str], f90_flags: list[str], linker_libpath: list[Path], linker_libname: list[str], define_macros: list[tuple[str, str]], undef_macros: list[str]):
+ self.module_name = module_name
+ self.numpy_install_path = numpy_install_path
+ self.build_template_path = numpy_install_path / "f2py" / "backends" / "src" / "meson.build.src"
+ self.sources = fortran_sources
+ self.numpy_get_include = numpy_get_include
+ self.f2py_get_include = f2py_get_include
+ self.wrappers = wrappers
+ self.dependencies = dependencies
+ self.include_directories = include_path
+ self.substitutions = {}
+ self.optimization_flags = optimization_flags
+ self.architecture_flags = architecture_flags
+ self.fortran_flags = f77_flags + f90_flags
+ self.linker_libpath = linker_libpath
+ self.linker_libname = linker_libname
+ self.define_macros = define_macros
+ self.undef_macros = undef_macros
+ self.pipeline = [self.initialize_template,
+ self.global_flags_substitution,
+ self.sources_substitution,
+ self.dependencies_substitution,
+ self.include_directories_subtitution,
+ self.linker_substitution,
+ self.macros_substitution]
+
+ @property
+ def meson_build_template(self) -> str:
+ if(not self.build_template_path.is_file()):
+ raise FileNotFoundError(errno.ENOENT, f"Meson build template {self.build_template_path.absolute()} does not exist.")
+ return self.build_template_path.read_text()
+
+ def initialize_template(self) -> None:
+ """Initialize with module name and external NumPy and F2PY C libraries."""
+ self.substitutions['modulename'] = self.module_name
+ self.substitutions['numpy_get_include'] = self.numpy_get_include.absolute()
+ self.substitutions['f2py_get_include'] = self.f2py_get_include.absolute()
+
+ def sources_substitution(self) -> None:
+ self.substitutions["source_list"] = ",".join(["\'"+str(source.absolute())+"\'" for source in self.sources])
+ self.substitutions["wrappers"] = ",".join(["\'"+str(wrapper.absolute())+"\'" for wrapper in self.wrappers])
+
+ def dependencies_substitution(self) -> None:
+ self.substitutions["dependencies_list"] = ", ".join([f"dependency('{dependecy}')" for dependecy in self.dependencies])
+
+ def include_directories_subtitution(self) -> None:
+ self.substitutions["include_directories_list"] = ", ".join([f"include_directories('{include_directory}')" for include_directory in self.include_directories])
+
+ def global_flags_substitution(self) -> None:
+ fortran_compiler_flags = self.fortran_flags + self.optimization_flags + self.architecture_flags
+ c_compiler_flags = self.optimization_flags + self.architecture_flags
+ self.substitutions["fortran_global_args"] = fortran_compiler_flags
+ self.substitutions["c_global_args"] = c_compiler_flags
+
+ def macros_substitution(self) -> None:
+ self.substitutions["macros"] = ""
+ if self.define_macros:
+ self.substitutions["macros"] = ",".join(f"\'-D{macro[0]}={macro[1]}\'" if macro[1] else f"-D{macro[0]}" for macro in self.define_macros)
+ if self.undef_macros:
+ self.substitutions["macros"] += "," + ",".join(f"\'-U{macro}\'" for macro in self.undef_macros)
+
+ def linker_substitution(self) -> None:
+ self.substitutions["linker_args"] = ""
+ if self.linker_libpath:
+ linker_libpath_subs = ",".join(f"-L{libpath}" for libpath in self.linker_libpath)
+ self.substitutions["linker_args"] += linker_libpath_subs
+ if self.linker_libname:
+ linker_libname_subs = ",".join(f"-l{libname}" for libname in self.linker_libname)
+ self.substitutions["linker_args"] += f",{linker_libname_subs}"
+
+ def generate_meson_build(self) -> str:
+ for node in self.pipeline:
+ node()
+ template = Template(self.meson_build_template)
+ return template.substitute(self.substitutions)
+
+
+class MesonBackend(Backend):
+
+ def __init__(self, module_name: str = 'untitled', fortran_compiler: str = None, c_compiler: str = None, f77exec: Path = None, f90exec: Path = None, f77_flags: list[str] = None, f90_flags: list[str] = None, include_paths: list[Path] = None, include_dirs: list[Path] = None, external_resources: list[str] = None, linker_libpath: list[Path] = None, linker_libname: list[str] = None, define_macros: list[tuple[str, str]] = None, undef_macros: list[str] = None, debug: bool = False, opt_flags: list[str] = None, arch_flags: list[str] = None, no_opt: bool = False, no_arch: bool = False) -> None:
+ self.meson_build_dir = "builddir"
+ if f77_flags is None:
+ f77_flags = []
+ if include_paths is None:
+ include_paths = []
+ if include_dirs is None:
+ include_dirs = []
+ if external_resources is None:
+ external_resources = []
+ if linker_libpath is None:
+ linker_libpath = []
+ if linker_libname is None:
+ linker_libname = []
+ if define_macros is None:
+ define_macros = []
+ if undef_macros is None:
+ undef_macros = []
+ if f77_flags is None:
+ f77_flags = []
+ if f90_flags is None:
+ f90_flags = []
+ if opt_flags is None:
+ opt_flags = []
+ if arch_flags is None:
+ arch_flags = []
+ super().__init__(module_name, fortran_compiler, c_compiler, f77exec, f90exec, f77_flags, f90_flags, include_paths, include_dirs, external_resources, linker_libpath, linker_libname, define_macros, undef_macros, debug, opt_flags, arch_flags, no_opt, no_arch)
+
+ self.wrappers: list[Path] = []
+ self.fortran_sources: list[Path] = []
+ self.template = Template(self.fortran_sources)
+
+ def _get_optimization_level(self):
+ if self.no_arch and not self.no_opt :
+ return 2
+ elif self.no_opt :
+ return 0
+ return 3
+
+ def _set_environment_variables(self) -> None:
+ if self.fortran_compiler:
+ os.putenv("FC", self.fortran_compiler)
+ elif self.f77exec:
+ os.putenv("FC", self.f77exec)
+ elif self.f90exec:
+ os.putenv("FC", self.f90exec)
+ if self.c_compiler:
+ os.putenv("CC", self.c_compiler)
+
+ def _move_exec_to_root(self, build_dir: Path):
+ walk_dir = build_dir / self.meson_build_dir
+ path_objects = walk_dir.glob(f"{self.module_name}*.so")
+ for path_object in path_objects:
+ shutil.move(path_object, Path.cwd())
+
+ def _get_build_command(self):
+ return ["meson", "setup", self.meson_build_dir, "-Ddebug=true" if self.debug else "-Ddebug=false", f"-Doptimization={str(self._get_optimization_level())}"]
+
+ def load_wrapper(self, wrappers: list[Path]) -> None:
+ self.wrappers = wrappers
+
+ def load_sources(self, fortran_sources: list[Path]) -> None:
+ for fortran_source in fortran_sources:
+ fortran_source = Path(fortran_source)
+ if not fortran_source.is_file():
+ raise FileNotFoundError(errno.ENOENT, f"{fortran_source.absolute()} does not exist.")
+ self.fortran_sources.append(fortran_source)
+
+ def write_meson_build(self, build_dir: Path) -> None:
+ """Writes the meson build file at specified location"""
+ meson_template = MesonTemplate(self.module_name, super().numpy_install_path(), self.numpy_get_include(), self.f2py_get_include(), self.wrappers, self.fortran_sources, self.external_resources, self.include_paths+self.include_dirs, optimization_flags=self.opt_flags, architecture_flags=self.arch_flags, f77_flags=self.f77_flags, f90_flags=self.f90_flags, linker_libpath=self.linker_libpath, linker_libname=self.linker_libname, define_macros=self.define_macros, undef_macros=self.undef_macros)
+ src = meson_template.generate_meson_build()
+ meson_build_file = build_dir / "meson.build"
+ meson_build_file.write_text(src)
+ return meson_build_file
+
+ def run_meson(self, build_dir: Path):
+ self._set_environment_variables()
+ 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)
+
+ def compile(self, f77_sources: list[Path], f90_sources:list[Path], object_files: list[Path], wrappers: list[Path], build_dir: Path) -> None:
+ self.load_wrapper(wrappers)
+ self.load_sources(f77_sources + f90_sources + object_files)
+ self.write_meson_build(build_dir)
+ self.run_meson(build_dir)
+ self._move_exec_to_root(build_dir)
diff --git a/numpy/f2py/backends/src/meson.build.src b/numpy/f2py/backends/src/meson.build.src
new file mode 100644
index 000000000000..42d33f973bc5
--- /dev/null
+++ b/numpy/f2py/backends/src/meson.build.src
@@ -0,0 +1,22 @@
+project('${modulename}', 'c',
+ version : '0.1',
+ default_options : ['warning_level=2'])
+
+add_languages('fortran')
+
+add_global_arguments(${c_global_args}, language: 'c')
+add_global_arguments(${fortran_global_args}, language: 'fortran')
+
+py_mod = import('python')
+py3 = py_mod.find_installation('python3')
+py3_dep = py3.dependency()
+message(py3.path())
+message(py3.get_install_dir())
+
+incdir_numpy = '${numpy_get_include}'
+
+incdir_f2py = '${f2py_get_include}'
+
+inc_np = include_directories(incdir_numpy, incdir_f2py)
+
+py3.extension_module('${modulename}', ${wrappers}, ${source_list}, incdir_f2py+'/fortranobject.c', include_directories: [inc_np, ${include_directories_list}], dependencies : [py3_dep, ${dependencies_list}], link_args : '${linker_args}', c_args : [${macros}], install : true)
\ No newline at end of file
diff --git a/numpy/f2py/capi_maps.py b/numpy/f2py/capi_maps.py
index f07066a09b67..f8c0f4ed975b 100644
--- a/numpy/f2py/capi_maps.py
+++ b/numpy/f2py/capi_maps.py
@@ -16,7 +16,7 @@
import copy
import re
-import os
+from pathlib import Path
from .crackfortran import markoutercomma
from . import cb_rules
@@ -185,9 +185,10 @@ def load_f2cmap_file(f2cmap_file):
if f2cmap_file is None:
# Default value
- f2cmap_file = '.f2py_f2cmap'
- if not os.path.isfile(f2cmap_file):
- return
+ f2cmap_file = Path.cwd() / Path('.f2py_f2cmap')
+ f2cmap_file = Path(f2cmap_file)
+ if not f2cmap_file.is_file():
+ return
# User defined additions to f2cmap_all.
# f2cmap_file must contain a dictionary of dictionaries, only. For
@@ -195,7 +196,7 @@ def load_f2cmap_file(f2cmap_file):
# interpreted as C 'float'. This feature is useful for F90/95 users if
# they use PARAMETERS in type specifications.
try:
- outmess('Reading f2cmap from {!r} ...\n'.format(f2cmap_file))
+ outmess('Reading f2cmap from {!r} ...\n'.format(f2cmap_file.name))
with open(f2cmap_file, 'r') as f:
d = eval(f.read().lower(), {}, {})
for k, d1 in d.items():
@@ -289,7 +290,7 @@ def getctype(var):
except KeyError:
errmess('getctype: "%s(kind=%s)" is mapped to C "%s" (to override define dict(%s = dict(%s="")) in %s/.f2py_f2cmap file).\n'
% (typespec, var['kindselector']['kind'], ctype,
- typespec, var['kindselector']['kind'], os.getcwd()))
+ typespec, var['kindselector']['kind'], Path.cwd()))
else:
if not isexternal(var):
errmess('getctype: No C-type found in "%s", assuming void.\n' % var)
diff --git a/numpy/f2py/diagnose.py b/numpy/f2py/diagnose.py
index 21ee399f035f..12acd24cbbbb 100644
--- a/numpy/f2py/diagnose.py
+++ b/numpy/f2py/diagnose.py
@@ -29,17 +29,17 @@ def run():
try:
import numpy
- has_newnumpy = 1
+ has_newnumpy = True
except ImportError:
print('Failed to import new numpy:', sys.exc_info()[1])
- has_newnumpy = 0
+ has_newnumpy = False
try:
- from numpy.f2py import f2py2e
- has_f2py2e = 1
+ from numpy.f2py import f2pyarg
+ has_f2pyarg = True
except ImportError:
- print('Failed to import f2py2e:', sys.exc_info()[1])
- has_f2py2e = 0
+ print('Failed to import f2pyarg:', sys.exc_info()[1])
+ has_f2pyarg = False
try:
import numpy.distutils
@@ -60,10 +60,10 @@ def run():
print('error:', msg)
print('------')
- if has_f2py2e:
+ if has_f2pyarg:
try:
- print('Found f2py2e version %r in %s' %
- (f2py2e.__version__.version, f2py2e.__file__))
+ print('Found f2pyarg version %r in %s' %
+ (f2pyarg.__version__.version, f2pyarg.__file__))
except Exception as msg:
print('error:', msg)
print('------')
diff --git a/numpy/f2py/f2pyarg.py b/numpy/f2py/f2pyarg.py
new file mode 100644
index 000000000000..847939187aa4
--- /dev/null
+++ b/numpy/f2py/f2pyarg.py
@@ -0,0 +1,885 @@
+#!/usr/bin/env python3
+
+"""
+argparse+logging front-end to f2py
+
+The concept is based around the idea that F2PY is overloaded in terms of
+functionality:
+
+1. Generating `.pyf` signature files
+2. Creating the wrapper `.c` files
+3. Compilation helpers
+ a. This essentially means `numpy.distutils` for now
+
+The three functionalities are largely independent of each other, hence the
+implementation in terms of subparsers
+"""
+
+from __future__ import annotations
+
+import sys
+import argparse
+import logging
+import os
+import pathlib
+import enum
+
+from numpy.version import version as __version__
+
+from .service import check_dccomp, check_npfcomp, check_dir, generate_files, segregate_files, get_f2py_modulename, wrapper_settings, compile_dist
+from .utils import open_build_dir
+from .auxfuncs import outmess
+from .backends import backends, Backend
+
+##################
+# Temp Variables #
+##################
+
+# TODO: Kill these np.distutil specific variables
+npd_link = ['atlas', 'atlas_threads', 'atlas_blas', 'atlas_blas_threads',
+ 'lapack_atlas', 'lapack_atlas_threads', 'atlas_3_10',
+ 'atlas_3_10_threads', 'atlas_3_10_blas', 'atlas_3_10_blas_threads'
+ 'lapack_atlas_3_10', 'lapack_atlas_3_10_threads', 'flame', 'mkl',
+ 'openblas', 'openblas_lapack', 'openblas_clapack', 'blis',
+ 'lapack_mkl', 'blas_mkl', 'accelerate', 'openblas64_',
+ 'openblas64__lapack', 'openblas_ilp64', 'openblas_ilp64_lapack'
+ 'x11', 'fft_opt', 'fftw', 'fftw2', 'fftw3', 'dfftw', 'sfftw',
+ 'fftw_threads', 'dfftw_threads', 'sfftw_threads', 'djbfft', 'blas',
+ 'lapack', 'lapack_src', 'blas_src', 'numpy', 'f2py', 'Numeric',
+ 'numeric', 'numarray', 'numerix', 'lapack_opt', 'lapack_ilp64_opt',
+ 'lapack_ilp64_plain_opt', 'lapack64__opt', 'blas_opt',
+ 'blas_ilp64_opt', 'blas_ilp64_plain_opt', 'blas64__opt',
+ 'boost_python', 'agg2', 'wx', 'gdk_pixbuf_xlib_2',
+ 'gdk-pixbuf-xlib-2.0', 'gdk_pixbuf_2', 'gdk-pixbuf-2.0', 'gdk',
+ 'gdk_2', 'gdk-2.0', 'gdk_x11_2', 'gdk-x11-2.0', 'gtkp_x11_2',
+ 'gtk+-x11-2.0', 'gtkp_2', 'gtk+-2.0', 'xft', 'freetype2', 'umfpack',
+ 'amd']
+
+debug_api = ['capi']
+
+
+# TODO: Compatibility helper, kill later
+# From 3.9 onwards should be argparse.BooleanOptionalAction
+class BoolAction(argparse.Action):
+ """A custom action to mimic Ruby's --[no]-blah functionality in f2py
+
+ This is meant to use ``argparse`` with a custom action to ensure backwards
+ compatibility with ``f2py``. Kanged `from here`_.
+
+ .. note::
+
+ Like in the old ``f2py``, it is not an error to pass both variants of
+ the flag, the last one will be used
+
+ .. from here:
+ https://thisdataguy.com/2017/07/03/no-options-with-argparse-and-python/
+ """
+
+ def __init__(self, option_strings, dest, nargs=None, **kwargs):
+ """Initialization of the boolean flag
+
+ Mimics the parent
+ """
+ super(BoolAction, self).__init__(option_strings, dest, nargs=0, **kwargs)
+
+ def __call__(self, parser, namespace, values, option_string: str=None):
+ """The logical negation action
+
+ Essentially this returns the semantically valid operation implied by
+ --no
+ """
+ setattr(namespace, self.dest, "no" not in option_string)
+
+
+# TODO: Generalize or kill this np.distutils specific helper action class
+class NPDLinkHelper(argparse.Action):
+ """A custom action to work with f2py's --link-blah
+
+ This is effectively the same as storing help_link
+
+ """
+
+ def __init__(self, option_strings, dest, nargs=None, **kwargs):
+ """Initialization of the boolean flag
+
+ Mimics the parent
+ """
+ super(NPDLinkHelper, self).__init__(option_strings, dest, nargs="*", **kwargs)
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ """The storage action
+
+ Essentially, split the value on -, store in dest
+
+ """
+ items = getattr(namespace, self.dest) or []
+ outvar = option_string.split("--link-")[1]
+ if outvar in npd_link:
+ # replicate the extend functionality
+ items.append(outvar)
+ setattr(namespace, self.dest, items)
+ else:
+ raise RuntimeError(f"{outvar} is not in {npd_link}")
+
+class DebugLinkHelper(argparse.Action):
+ """A custom action to work with f2py's --debug-blah"""
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ """The storage action
+
+ Essentially, split the value on -, store in dest
+
+ """
+ items = getattr(namespace, self.dest) or []
+ outvar = option_string.split("--debug-")[1]
+ if outvar in debug_api:
+ items.append(outvar)
+ setattr(namespace, self.dest, items)
+ else:
+ raise RuntimeError(f"{outvar} is not in {debug_api}")
+
+class ProcessMacros(argparse.Action):
+ """Process macros in the form of -Dmacro=value and -Dmacro"""
+
+ def __init__(self, option_strings, dest, nargs="*", **kwargs):
+ """Initialization of the boolean flag
+
+ Mimics the parent
+ """
+ super(ProcessMacros, self).__init__(option_strings, dest, nargs="*", **kwargs)
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ """The storage action
+
+ Essentially, split the value on -D, store in dest
+
+ """
+ items = getattr(namespace, self.dest) or []
+ for value in values:
+ if('=' in value):
+ items.append((value.split("=")[0], value.split("=")[1]))
+ else:
+ items.append((value, None))
+ setattr(namespace, self.dest, items)
+
+class EnumAction(argparse.Action):
+ """
+ Argparse action for handling Enums
+ """
+ def __init__(self, **kwargs):
+ # Pop off the type value
+ enum_type = kwargs.pop("type", None)
+
+ # Ensure an Enum subclass is provided
+ if enum_type is None:
+ raise ValueError("type must be assigned an Enum when using EnumAction")
+ if not issubclass(enum_type, enum.Enum):
+ raise TypeError("type must be an Enum when using EnumAction")
+
+ # Generate choices from the Enum
+ kwargs.setdefault("choices", tuple(e.value for e in enum_type))
+
+ super(EnumAction, self).__init__(**kwargs)
+
+ self._enum = enum_type
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ # Convert value back into an Enum
+ value = self._enum(values)
+ setattr(namespace, self.dest, value)
+
+class IncludePathAction(argparse.Action):
+ """Custom action to extend paths when --include-paths : is called"""
+ def __init__(self, option_strings, dest, nargs="?", **kwargs):
+ """Initialization of the --include-paths flag
+
+ Mimics the parent
+ """
+ super(IncludePathAction, self).__init__(option_strings, dest, nargs="?", **kwargs)
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ """Split the paths by ':' convert them to path and append them to the attribute"""
+ items = getattr(namespace, self.dest) or []
+ if values:
+ items.extend([pathlib.Path(path) for path in values.split(os.pathsep)])
+ setattr(namespace, self.dest, items)
+
+class ParseStringFlags(argparse.Action):
+ """Custom action to parse and store flags passed as string
+ Ex-
+ f2py --opt="-DDEBUG=1 -O" will be stored as ["-DDEBUG=1", "-O"]"""
+
+ def __init__(self, option_strings, dest, nargs="1", **kwargs):
+ """Initialization of the flag, mimics the parent"""
+ super(ParseStringFlags, self).__init__(option_strings, dest, nargs=1, **kwargs)
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ """The storage action, mimics the parent"""
+ items = getattr(namespace, self.dest) or []
+ items.extend(value.split(' ') for value in values)
+ setattr(namespace, self.dest, items)
+
+class Backends(enum.Enum):
+ Meson = "meson"
+ Distutils = "distutils"
+
+##########
+# Parser #
+##########
+
+
+parser = argparse.ArgumentParser(
+ prog="f2py",
+ description="""
+ This program generates a Python C/API file (module.c) that
+ contains wrappers for given fortran functions so that they can be called
+ from Python.
+
+ With the -c option the corresponding extension modules are built.""",
+ add_help=False, # Since -h is taken...
+ # Format to look like f2py
+ formatter_class=lambda prog: argparse.RawDescriptionHelpFormatter(
+ prog, max_help_position=100, width=85
+ ),
+ epilog=f"""
+ Using the following macros may be required with non-gcc Fortran
+ compilers:
+ -DPREPEND_FORTRAN -DNO_APPEND_FORTRAN -DUPPERCASE_FORTRAN
+ -DUNDERSCORE_G77
+
+ When using -DF2PY_REPORT_ATEXIT, a performance report of F2PY
+ interface is printed out at exit (platforms: Linux).
+
+ When using -DF2PY_REPORT_ON_ARRAY_COPY=, a message is
+ sent to stderr whenever F2PY interface makes a copy of an
+ array. Integer sets the threshold for array sizes when
+ a message should be shown.
+
+ Version: {__version__}
+ numpy Version: {__version__}
+ Requires: Python 3.5 or higher.
+ License: NumPy license (see LICENSE.txt in the NumPy source code)
+ Copyright 1999 - 2011 Pearu Peterson all rights reserved.
+ https://web.archive.org/web/20140822061353/http://cens.ioc.ee/projects/f2py2e
+ """
+)
+
+# subparsers = parser.add_subparsers(help="Functional subsets")
+build_helpers = parser.add_argument_group("build helpers, only with -c")
+generate_wrappers = parser.add_argument_group("wrappers and signature files")
+
+# Common #
+##########
+
+# --help is still free
+parser.add_argument("--help", action="store_true", help="Print the help")
+
+# TODO: Remove?
+
+parser.add_argument(
+ "Fortran Files",
+ metavar="",
+ action="extend", # list storage
+ nargs="*",
+ help="""Paths to fortran/signature files that will be scanned for
+ in order to determine their signatures.""",
+)
+
+parser.add_argument(
+ "Skip functions",
+ metavar="skip:",
+ action="extend",
+ type=str,
+ nargs="*",
+ help="Ignore fortran functions that follow until `:'.",
+)
+
+parser.add_argument(
+ "Keep functions",
+ metavar="only:",
+ action="extend",
+ type=str,
+ nargs="*",
+ help="Use only fortran functions that follow until `:'.",
+)
+
+parser.add_argument(
+ "-m",
+ "--module",
+ metavar="",
+ type=str,
+ nargs=1,
+ help="""Name of the module; f2py generates a Python/C API
+ file module.c or extension module .
+ Default is 'untitled'.""",
+)
+
+parser.add_argument(
+ "--lower",
+ "--no-lower",
+ metavar="--[no-]lower",
+ action=BoolAction,
+ default=False,
+ type=bool,
+ help="""Do [not] lower the cases in .
+ By default, --lower is assumed with -h key, and --no-lower without -h
+ key.""",
+)
+
+parser.add_argument(
+ "-b",
+ "--build-dir",
+ metavar="",
+ type=check_dir,
+ nargs=1,
+ help="""All f2py generated files are created in .
+ Default is tempfile.mkdtemp().""",
+)
+
+parser.add_argument(
+ "-o",
+ "--overwrite-signature",
+ action="store_true",
+ help="Overwrite existing signature file.",
+)
+
+parser.add_argument(
+ "--latex-doc",
+ "--no-latex-doc",
+ metavar="--[no-]latex-doc",
+ action=BoolAction,
+ type=bool,
+ default=False,
+ nargs=1,
+ help="""Create (or not) module.tex.
+ Default is --no-latex-doc.""",
+)
+
+parser.add_argument(
+ "--short-latex",
+ action="store_true",
+ help="""Create 'incomplete' LaTeX document (without commands
+ \\documentclass, \\tableofcontents, and \\begin{{document}},
+ \\end{{document}}).""",
+)
+
+parser.add_argument(
+ "--rest-doc",
+ "--no-rest-doc",
+ metavar="--[no-]rest-doc",
+ action=BoolAction,
+ type=bool,
+ default=False,
+ nargs=1,
+ help="""Create (or not) module.rst.
+ Default is --no-rest-doc.""",
+)
+
+parser.add_argument(
+ "--debug-capi",
+ dest="debug_api",
+ default=[],
+ nargs="*",
+ action=DebugLinkHelper,
+ help="""Create C/API code that reports the state of the wrappers
+ during runtime. Useful for debugging.""",
+)
+
+parser.add_argument(
+ "--wrap-functions",
+ "--no-wrap-functions",
+ metavar="--[no-]wrap-functions",
+ action=BoolAction,
+ type=bool,
+ default=True,
+ nargs=1,
+ help="""Create (or not) Fortran subroutine wrappers to Fortran 77
+ functions. Default is --wrap-functions because it
+ ensures maximum portability/compiler independence""",
+)
+
+parser.add_argument(
+ "--include-paths",
+ nargs='?',
+ dest="include_paths",
+ action=IncludePathAction,
+ metavar=":",
+ type=str,
+ default=[],
+ help="Search include files from the given directories.",
+)
+
+parser.add_argument(
+ "--help-link",
+ metavar="..",
+ action="extend",
+ nargs="*",
+ choices=npd_link,
+ type=str,
+ help="""List system resources found by system_info.py. See also
+ --link- switch below. [..] is optional list
+ of resources names. E.g. try 'f2py --help-link lapack_opt'."""
+)
+
+parser.add_argument(
+ "--f2cmap",
+ metavar="",
+ type=pathlib.Path,
+ default=".f2py_f2cmap",
+ help="""Load Fortran-to-Python KIND specification from the given
+ file. Default: .f2py_f2cmap in current directory.""",
+)
+
+parser.add_argument(
+ "--quiet",
+ action="store_true",
+ help="Run quietly.",
+)
+
+parser.add_argument(
+ "--verbose",
+ action="store_true",
+ default=True,
+ help="Run with extra verbosity.",
+)
+
+parser.add_argument(
+ "-v",
+ action="store_true",
+ dest="version",
+ help="Print f2py version ID and exit.",
+)
+
+# Wrappers/Signatures #
+#######################
+
+generate_wrappers.add_argument(
+ # TODO: Seriously consider scrapping this naming convention
+ "-h",
+ "--hint-signature",
+ metavar="",
+ type=pathlib.Path,
+ nargs=1,
+ help="""
+ Write signatures of the fortran routines to file and exit. You
+ can then edit and use it instead of . If
+ ==stdout then the signatures are printed to stdout.
+ """,
+)
+
+# NumPy Distutils #
+###################
+
+# TODO: Generalize to allow -c to take other build systems with numpy.distutils
+# as a default
+build_helpers.add_argument(
+ "-c",
+ default=False,
+ action="store_true",
+ help="Compilation (via NumPy distutils)"
+)
+
+build_helpers.add_argument(
+ "--fcompiler",
+ nargs=1,
+ type=check_npfcomp,
+ help="Specify Fortran compiler type by vendor"
+)
+
+build_helpers.add_argument(
+ "--compiler",
+ nargs=1,
+ type=check_dccomp,
+ help="Specify distutils C compiler type"
+)
+
+build_helpers.add_argument(
+ "--help-fcompiler",
+ action="store_true",
+ help="List available Fortran compilers and exit"
+)
+
+build_helpers.add_argument(
+ "--f77exec",
+ nargs=1,
+ type=pathlib.Path,
+ help="Specify the path to a F77 compiler"
+)
+
+build_helpers.add_argument(
+ "--f90exec",
+ nargs=1,
+ type=pathlib.Path,
+ help="Specify the path to a F90 compiler"
+)
+
+build_helpers.add_argument(
+ "--f77flags",
+ nargs=1,
+ action=ParseStringFlags,
+ help="Specify F77 compiler flags"
+)
+
+build_helpers.add_argument(
+ "--f90flags",
+ nargs=1,
+ action=ParseStringFlags,
+ help="Specify F90 compiler flags"
+)
+
+build_helpers.add_argument(
+ "--opt",
+ "--optimization_flags",
+ nargs=1,
+ type=str,
+ action=ParseStringFlags,
+ help="Specify optimization flags"
+)
+
+build_helpers.add_argument(
+ "--arch",
+ "--architecture_optimizations",
+ nargs=1,
+ type=str,
+ action=ParseStringFlags,
+ help="Specify architecture specific optimization flags"
+)
+
+build_helpers.add_argument(
+ "--noopt",
+ action="store_true",
+ help="Compile without optimization"
+)
+
+build_helpers.add_argument(
+ "--noarch",
+ action="store_true",
+ help="Compile without arch-dependent optimization"
+)
+
+build_helpers.add_argument(
+ "--debug",
+ action="store_true",
+ help="Compile with debugging information"
+)
+
+build_helpers.add_argument(
+ "-L",
+ "--library-path",
+ type=pathlib.Path,
+ metavar="/path/to/lib/",
+ nargs=1,
+ action="extend",
+ default=[],
+ help="Path to library"
+)
+
+build_helpers.add_argument(
+ "-U",
+ type=str,
+ nargs="*",
+ action="extend",
+ dest='undef_macros',
+ help="Undefined macros"
+)
+
+build_helpers.add_argument(
+ "-D",
+ type=str,
+ metavar='MACRO[=value]',
+ nargs="*",
+ action=ProcessMacros,
+ dest="define_macros",
+ help="Define macros"
+)
+
+build_helpers.add_argument(
+ "-l",
+ "--library_name",
+ type=str,
+ metavar="",
+ nargs=1,
+ action="extend",
+ help="Library name"
+)
+
+build_helpers.add_argument(
+ "-I",
+ "--include_dirs",
+ type=pathlib.Path,
+ metavar="/path/to/include",
+ nargs="*",
+ default=[],
+ action="extend",
+ help="Include directories"
+)
+
+# TODO: Kill this ASAP
+# Also collect in to REMAINDER and extract from there
+# Flag not working. To be debugged.
+build_helpers.add_argument(
+ '--link-atlas', '--link-atlas_threads', '--link-atlas_blas',
+ '--link-atlas_blas_threads', '--link-lapack_atlas',
+ '--link-lapack_atlas_threads', '--link-atlas_3_10',
+ '--link-atlas_3_10_threads', '--link-atlas_3_10_blas',
+ '--link-atlas_3_10_blas_threadslapack_atlas_3_10',
+ '--link-lapack_atlas_3_10_threads', '--link-flame', '--link-mkl',
+ '--link-openblas', '--link-openblas_lapack', '--link-openblas_clapack',
+ '--link-blis', '--link-lapack_mkl', '--link-blas_mkl', '--link-accelerate',
+ '--link-openblas64_', '--link-openblas64__lapack', '--link-openblas_ilp64',
+ '--link-openblas_ilp64_lapackx11', '--link-fft_opt', '--link-fftw',
+ '--link-fftw2', '--link-fftw3', '--link-dfftw', '--link-sfftw',
+ '--link-fftw_threads', '--link-dfftw_threads', '--link-sfftw_threads',
+ '--link-djbfft', '--link-blas', '--link-lapack', '--link-lapack_src',
+ '--link-blas_src', '--link-numpy', '--link-f2py', '--link-Numeric',
+ '--link-numeric', '--link-numarray', '--link-numerix', '--link-lapack_opt',
+ '--link-lapack_ilp64_opt', '--link-lapack_ilp64_plain_opt',
+ '--link-lapack64__opt', '--link-blas_opt', '--link-blas_ilp64_opt',
+ '--link-blas_ilp64_plain_opt', '--link-blas64__opt', '--link-boost_python',
+ '--link-agg2', '--link-wx', '--link-gdk_pixbuf_xlib_2',
+ '--link-gdk-pixbuf-xlib-2.0', '--link-gdk_pixbuf_2', '--link-gdk-pixbuf-2.0',
+ '--link-gdk', '--link-gdk_2', '--link-gdk-2.0', '--link-gdk_x11_2',
+ '--link-gdk-x11-2.0', '--link-gtkp_x11_2', '--link-gtk+-x11-2.0',
+ '--link-gtkp_2', '--link-gtk+-2.0', '--link-xft', '--link-freetype2',
+ '--link-umfpack', '--link-amd',
+ metavar="--link-",
+ dest="link_resource",
+ default=[],
+ nargs="*",
+ action=NPDLinkHelper,
+ help="The link helpers for numpy distutils"
+)
+
+parser.add_argument('--backend',
+ type=Backends,
+ default=Backends.Distutils,
+ action=EnumAction)
+
+# The rest, only works for files, since we expect:
+# .o .so .a
+parser.add_argument('otherfiles',
+ type=pathlib.Path,
+ nargs=argparse.REMAINDER)
+
+
+################
+# Main Process #
+################
+
+def get_additional_headers(rem: list[str]) -> list[str]:
+ return [val[8:] for val in rem if val[:8] == '-include']
+
+def get_f2pyflags_dist(args: argparse.Namespace, skip_funcs: list[str], only_funcs: list[str]) -> list[str]:
+ # Distutils requires 'f2py_options' which will be a subset of
+ # sys.argv array received. This function reconstructs the array
+ # from received args.
+ f2py_flags = []
+ if(args.wrap_functions):
+ f2py_flags.append('--wrap-functions')
+ else:
+ f2py_flags.append('--no-wrap-functions')
+ if(args.lower):
+ f2py_flags.append('--lower')
+ else:
+ f2py_flags.append('--no-lower')
+ if(args.debug_api):
+ f2py_flags.append('--debug-capi')
+ if(args.quiet):
+ f2py_flags.append('--quiet')
+ f2py_flags.append("--skip-empty-wrappers")
+ if(skip_funcs):
+ f2py_flags.extend(['skip:']+skip_funcs + [':'])
+ if(only_funcs):
+ f2py_flags.extend(['only:']+only_funcs + [':'])
+ if(args.include_paths):
+ f2py_flags.extend(['--include-paths']+[str(include_path) for include_path in args.include_paths])
+ if(args.f2cmap):
+ f2py_flags.extend(['--f2cmap', str(args.f2cmap)])
+ return f2py_flags
+
+def get_fortran_library_flags(args: argparse.Namespace) -> list[str]:
+ flib_flags = []
+ if args.fcompiler:
+ flib_flags.append(f'--fcompiler={args.fcompiler[0]}')
+ if args.compiler:
+ flib_flags.append(f'--compiler={args.compiler[0]}')
+ return flib_flags
+
+def get_fortran_compiler_flags(args: argparse.Namespace) -> list[str]:
+ fc_flags = []
+ if(args.help_fcompiler):
+ fc_flags.append('--help-fcompiler')
+ if(args.f77exec):
+ fc_flags.append(f'--f77exec={str(args.f77exec[0])}')
+ if(args.f90exec):
+ fc_flags.append(f'--f90exec={str(args.f90exec[0])}')
+ if(args.f77flags):
+ fc_flags.append(f'--f77flags={" ".join(args.f77flags)}')
+ if(args.f90flags):
+ fc_flags.append(f'--f90flags={" ".join(args.f90flags)}')
+ if(args.arch):
+ fc_flags.append(f'--arch={" ".join(args.arch)}')
+ if(args.opt):
+ fc_flags.append(f'--opt={" ".join(args.opt)}')
+ if(args.noopt):
+ fc_flags.append('--noopt')
+ if(args.noarch):
+ fc_flags.append('--noarch')
+ if(args.debug):
+ fc_flags.append('--debug')
+
+
+def get_module_name(args: argparse.Namespace, pyf_files: list[str]) -> str:
+ if(args.module is not None):
+ return args.module[0]
+ if args.c:
+ for file in pyf_files:
+ if name := get_f2py_modulename(file):
+ return name
+ return "unititled"
+ return ""
+
+def get_signature_file(args: argparse.Namespace, build_dir: pathlib.Path) -> pathlib.Path:
+ sign_file = None
+ if(args.hint_signature):
+ sign_file = build_dir / args.hint_signature[0]
+ if sign_file and sign_file.is_file() and not args.overwrite_signature:
+ print(f'Signature file "{sign_file}" exists!!! Use --overwrite-signature to overwrite.')
+ parser.exit()
+ return sign_file
+
+def segregate_posn_args(args: argparse.Namespace) -> tuple[list[str], list[str], list[str]]:
+ # Currently, argparse does not recognise 'skip:' and 'only:' as optional args
+ # and clubs them all in "Fortran Files" attr. This function segregates them.
+ funcs = {"skip:": [], "only:": []}
+ mode = "file"
+ files = []
+ for arg in getattr(args, "Fortran Files"):
+ if arg in funcs:
+ mode = arg
+ elif arg == ':' and mode in funcs:
+ mode = "file"
+ elif mode == "file":
+ files.append(arg)
+ else:
+ funcs[mode].append(arg)
+ return files, funcs['skip:'], funcs['only:']
+
+def process_args(args: argparse.Namespace, rem: list[str]) -> None:
+ if args.help:
+ parser.print_help()
+ parser.exit()
+ if(args.version):
+ outmess(__version__)
+ parser.exit()
+
+ # Step 1: Segregate input files from 'skip:' and 'only:' args
+ # Read comments in the 'segregate_posn_args' function for more detail
+ files, skip_funcs, only_funcs = segregate_posn_args(args)
+
+ # Step 2: Segregate source source files based on their extensions
+ f77_files, f90_files, pyf_files, obj_files, other_files = segregate_files(files)
+
+ # Step 3: Open the correct build directory. Read 'open_build_dir' docstring for more detail
+ with open_build_dir(args.build_dir, args.c) as build_dir:
+ # Step 4: Get module name and signature file path
+ module_name = get_module_name(args, pyf_files)
+ sign_file = get_signature_file(args, build_dir)
+
+ # Step 5: Parse '-include' flags and store s in a list
+ # since argparse can't handle '-include'
+ # we filter it out into rem and parse it manually.
+ headers = get_additional_headers(rem)
+ # TODO: Refine rules settings. Read codebase and remove unused ones
+
+ # Step 6: Generate settings dictionary for f2py internal files
+ # The variables in `rules.py`, `crackfortran.py`,
+ # `capy_maps.py` and `auxfuncs.py` are set using
+ # information in these dictionaries.
+ # These are the same which 'f2py2e' passes to internal files
+ rules_setts = {
+ 'module': module_name,
+ 'buildpath': build_dir,
+ 'dorestdoc': args.rest_doc,
+ 'dolatexdoc': args.latex_doc,
+ 'shortlatex': args.short_latex,
+ 'verbose': args.verbose,
+ 'do-lower': args.lower,
+ 'f2cmap_file': args.f2cmap,
+ 'include_paths': args.include_paths,
+ 'coutput': None,
+ 'f2py_wrapper_output': None,
+ 'emptygen': False,
+ }
+ crackfortran_setts = {
+ 'module': module_name,
+ 'skipfuncs': skip_funcs,
+ 'onlyfuncs': only_funcs,
+ 'verbose': args.verbose,
+ 'include_paths': args.include_paths,
+ 'do-lower': args.lower,
+ 'debug': args.debug_api,
+ 'wrapfuncs': args.wrap_functions,
+ }
+ capi_maps_setts = {
+ 'f2cmap': args.f2cmap,
+ 'headers': headers,
+ }
+ auxfuncs_setts = {
+ 'verbose': args.verbose,
+ 'debug': args.debug_api,
+ 'wrapfuncs': args.wrap_functions,
+ }
+
+ # The function below sets the global and module variables in internal files
+ # Read the comments inside this function for explanation
+ wrapper_settings(rules_setts, crackfortran_setts, capi_maps_setts, auxfuncs_setts)
+
+ # Step 7: If user has asked for compilation. Mimic 'run_compile' from f2py2e
+ # Disutils receives all the options and builds the extension.
+ if(args.c and args.backend == Backends.Distutils):
+ link_resource = args.link_resource
+
+ # The 3 functions below generate arrays of flag similar to how
+ # 'run_compile()' segregates flags into different arrays
+ f2py_flags = get_f2pyflags_dist(args, skip_funcs, only_funcs)
+ fc_flags = get_fortran_compiler_flags(args)
+ flib_flags = get_fortran_library_flags(args)
+
+ # The array of flags from above is passed to distutils where
+ # it is handled internally
+ ext_args = {
+ 'name': module_name,
+ 'sources': pyf_files + f77_files + f90_files,
+ 'include_dirs': [include_dir.absolute() for include_dir in args.include_dirs],
+ 'library_dirs': [lib_path.absolute() for lib_path in args.library_path],
+ 'libraries': args.library_name,
+ 'define_macros': args.define_macros,
+ 'undef_macros': args.undef_macros,
+ 'extra_objects': obj_files,
+ 'f2py_options': f2py_flags,
+ }
+ compile_dist(ext_args, link_resource, build_dir, fc_flags, flib_flags, args.quiet)
+ else:
+ # Step 8: Generate wrapper or signature file if compile flag is not given
+ wrappers = generate_files(f77_files + f90_files + pyf_files, module_name, sign_file)
+ if wrappers and args.c:
+ backend: Backend = backends.get(args.backend.value)(module_name=module_name, include_dirs=args.include_dirs, include_paths=args.include_paths, external_resources=args.link_resource, debug=args.debug, arch_flags=args.arch, opt_flags=args.opt, f77_flags=args.f77flags, f90_flags=args.f90flags, linker_libpath=args.library_path, linker_libname=args.library_name, define_macros=args.define_macros, undef_macros=args.undef_macros)
+ backend.compile(f77_files, f90_files, obj_files, wrappers, build_dir)
+
+def sort_args(args: list[str]) -> list[str]:
+ """Sort files at the end of the list"""
+ extensions = (".f", ".for", ".ftn", ".f77", ".f90", ".f95", ".f03", ".f08", ".pyf", ".src", ".o", ".out", ".so", ".a")
+ return sorted(args, key=lambda arg: arg.endswith(extensions))
+
+def main():
+ logger = logging.getLogger("f2py_cli")
+ logger.setLevel(logging.WARNING)
+ sys.argv = sort_args(sys.argv)
+ args, rem = parser.parse_known_args()
+ # since argparse can't handle '-include'
+ # we filter it out into rem and parse it manually.
+ process_args(args, rem)
+
+if __name__ == "__main__":
+ main()
diff --git a/numpy/f2py/service.py b/numpy/f2py/service.py
new file mode 100644
index 000000000000..5a38aa445ff0
--- /dev/null
+++ b/numpy/f2py/service.py
@@ -0,0 +1,350 @@
+from __future__ import annotations
+
+import sys
+import logging
+import re
+
+from pathlib import Path, PurePath
+from typing import Any, Optional
+
+# Distutil dependencies
+from numpy.distutils.misc_util import dict_append
+from numpy.distutils.system_info import get_info
+from numpy.distutils.core import setup, Extension
+
+from . import crackfortran
+from . import capi_maps
+from . import rules
+from . import auxfuncs
+from . import cfuncs
+from . import cb_rules
+
+from .utils import get_f2py_dir
+
+outmess = auxfuncs.outmess
+
+logger = logging.getLogger("f2py_cli")
+logger.setLevel(logging.WARNING)
+
+F2PY_MODULE_NAME_MATCH = re.compile(r'\s*python\s*module\s*(?P[\w_]+)',
+ re.I).match
+F2PY_USER_MODULE_NAME_MATCH = re.compile(r'\s*python\s*module\s*(?P[\w_]*?'
+ r'__user__[\w_]*)', re.I).match
+
+def check_fortran(fname: str) -> Path:
+ """Function which checks
+
+ This is meant as a sanity check, but will not raise an error, just a
+ warning. It is called with ``type``
+
+ Parameters
+ ----------
+ fname : str
+ The name of the file
+
+ Returns
+ -------
+ pathlib.Path
+ This is the string as a path, irrespective of the suffix
+ """
+ fpname = Path(fname)
+ if fpname.suffix.lower() not in [".f90", ".f", ".f77"]:
+ logger.warning(
+ """Does not look like a standard fortran file ending in *.f90, *.f or
+ *.f77, continuing against better judgement"""
+ )
+ return fpname
+
+
+def check_dir(dname: str) -> Optional[Path]:
+ """Function which checks the build directory
+
+ This is meant to ensure no odd directories are passed, it will fail if a
+ file is passed. Creates directory if not present.
+
+ Parameters
+ ----------
+ dname : str
+ The name of the directory, by default it will be a temporary one
+
+ Returns
+ -------
+ pathlib.Path
+ This is the string as a path
+ """
+ if dname:
+ dpname = Path(dname)
+ dpname.mkdir(parents=True, exist_ok=True)
+ return dpname
+ return None
+
+
+def check_dccomp(opt: str) -> str:
+ """Function which checks for an np.distutils compliant c compiler
+
+ Meant to enforce sanity checks, note that this just checks against distutils.show_compilers()
+
+ Parameters
+ ----------
+ opt: str
+ The compiler name, must be a distutils option
+
+ Returns
+ -------
+ str
+ This is the option as a string
+ """
+ cchoices = ["bcpp", "cygwin", "mingw32", "msvc", "unix"]
+ if opt in cchoices:
+ return opt
+ else:
+ raise RuntimeError(f"{opt} is not an distutils supported C compiler, choose from {cchoices}")
+
+
+def check_npfcomp(opt: str) -> str:
+ """Function which checks for an np.distutils compliant fortran compiler
+
+ Meant to enforce sanity checks
+
+ Parameters
+ ----------
+ opt: str
+ The compiler name, must be a np.distutils option
+
+ Returns
+ -------
+ str
+ This is the option as a string
+ """
+ from numpy.distutils import fcompiler
+ fcompiler.load_all_fcompiler_classes()
+ fchoices = list(fcompiler.fcompiler_class.keys())
+ if opt in fchoices[0]:
+ return opt
+ else:
+ raise RuntimeError(f"{opt} is not an np.distutils supported compiler, choose from {fchoices}")
+
+
+def _set_additional_headers(headers: list[str]) -> None:
+ for header in headers:
+ cfuncs.outneeds['userincludes'].append(header[1:-1])
+ cfuncs.userincludes[header[1:-1]] = f"#include {header}"
+
+def _set_crackfortran(crackfortran_setts: dict[str, Any]) -> None:
+ crackfortran.reset_global_f2py_vars()
+ crackfortran.f77modulename = crackfortran_setts["module"]
+ crackfortran.include_paths[:] = crackfortran_setts["include_paths"]
+ crackfortran.debug = crackfortran_setts["debug"]
+ crackfortran.verbose = crackfortran_setts["verbose"]
+ crackfortran.skipfuncs = crackfortran_setts["skipfuncs"]
+ crackfortran.onlyfuncs = crackfortran_setts["onlyfuncs"]
+ crackfortran.dolowercase = crackfortran_setts["do-lower"]
+
+def _set_rules(rules_setts: dict[str, Any]) -> None:
+ rules.options = rules_setts
+
+def _set_capi_maps(capi_maps_setts: dict[str, Any]) -> None:
+ capi_maps.load_f2cmap_file(capi_maps_setts["f2cmap"])
+ _set_additional_headers(capi_maps_setts["headers"])
+
+def _set_auxfuncs(aux_funcs_setts: dict[str, Any]) -> None:
+ auxfuncs.options = {'verbose': aux_funcs_setts['verbose']}
+ auxfuncs.debugoptions = aux_funcs_setts["debug"]
+ auxfuncs.wrapfuncs = aux_funcs_setts['wrapfuncs']
+
+def _dict_append(d_out: dict[str, Any], d_in: dict[str, Any]) -> None:
+ for (k, v) in d_in.items():
+ if k not in d_out:
+ d_out[k] = []
+ if isinstance(v, list):
+ d_out[k] = d_out[k] + v
+ else:
+ d_out[k].append(v)
+
+def _buildmodules(lst: list[dict[str, Any]]) -> dict[str, Any]:
+ cfuncs.buildcfuncs()
+ outmess('Building modules...\n')
+ modules, mnames = [], []
+ isusedby: dict[str, list[Any]] = {}
+ for item in lst:
+ if '__user__' in item['name']:
+ cb_rules.buildcallbacks(item)
+ else:
+ if 'use' in item:
+ for u in item['use'].keys():
+ if u not in isusedby:
+ isusedby[u] = []
+ isusedby[u].append(item['name'])
+ modules.append(item)
+ mnames.append(item['name'])
+ ret: dict[str, Any] = {}
+ for module, name in zip(modules, mnames):
+ if name in isusedby:
+ outmess('\tSkipping module "%s" which is used by %s.\n' % (
+ name, ','.join('"%s"' % s for s in isusedby[name])))
+ else:
+ um = []
+ if 'use' in module:
+ for u in module['use'].keys():
+ if u in isusedby and u in mnames:
+ um.append(modules[mnames.index(u)])
+ else:
+ outmess(
+ f'\tModule "{name}" uses nonexisting "{u}" '
+ 'which will be ignored.\n')
+ ret[name] = {}
+ _dict_append(ret[name], rules.buildmodule(module, um))
+ return ret
+
+
+def _generate_signature(postlist: list[dict[str, Any]], sign_file: Path) -> None:
+ outmess(f"Saving signatures to file {sign_file}" + "\n")
+ pyf = crackfortran.crack2fortran(postlist)
+ if sign_file in {"-", "stdout"}:
+ sys.stdout.write(pyf)
+ else:
+ with open(sign_file, "w") as f:
+ f.write(pyf)
+
+def _check_postlist(postlist: list[dict[str, Any]], sign_file: Path) -> None:
+ isusedby: dict[str, list[Any]] = {}
+ for plist in postlist:
+ if 'use' in plist:
+ for u in plist['use'].keys():
+ if u not in isusedby:
+ isusedby[u] = []
+ isusedby[u].append(plist['name'])
+ for plist in postlist:
+ if plist['block'] == 'python module' and '__user__' in plist['name'] and plist['name'] in isusedby:
+ outmess(
+ f'Skipping Makefile build for module "{plist["name"]}" '
+ 'which is used by {}\n'.format(
+ ','.join(f'"{s}"' for s in isusedby[plist['name']])))
+ if(sign_file):
+ outmess(
+ 'Stopping. Edit the signature file and then run f2py on the signature file: ')
+ outmess('%s %s\n' %
+ (PurePath(sys.argv[0]).name, sign_file))
+ return
+ for plist in postlist:
+ if plist['block'] != 'python module':
+ outmess(
+ 'Tip: If your original code is Fortran source then you must use -m option.\n')
+
+def _callcrackfortran(files: list[str], module_name: str) -> list[dict[str, Any]]:
+ postlist = crackfortran.crackfortran([str(file) for file in files])
+ for mod in postlist:
+ mod["coutput"] = f"{mod['name']}module.c"
+ mod["f2py_wrapper_output"] = f"{mod['name']}-f2pywrappers.f"
+ return postlist
+
+def _set_dependencies_dist(ext_args: dict[str, Any], link_resource: list[str]) -> None:
+ for dep in link_resource:
+ info = get_info(dep)
+ if not info:
+ outmess('No %s resources found in system'
+ ' (try `f2py --help-link`)\n' % (repr(dep)))
+ dict_append(ext_args, **info)
+
+def get_f2py_modulename(source: str) -> Optional[str]:
+ name = None
+ with open(source) as f:
+ for line in f:
+ if m := F2PY_MODULE_NAME_MATCH(line):
+ if F2PY_USER_MODULE_NAME_MATCH(line): # skip *__user__* names
+ continue
+ name = m.group('name')
+ break
+ return name
+
+def wrapper_settings(rules_setts: dict[str, Any], crackfortran_setts: dict[str, Any], capi_maps_setts: dict[str, Any], auxfuncs_setts: dict[str, Any]) -> Optional[Path]:
+ # This function also mimics f2py2e. I have added the link to specific code blocks that each function below mimics.
+ # Step 6.1: https://github.com/numpy/numpy/blob/45bc13e6d922690eea43b9d807d476e0f243f836/numpy/f2py/f2py2e.py#L331
+ _set_rules(rules_setts)
+ # Step 6.2: https://github.com/numpy/numpy/blob/main/numpy/f2py/f2py2e.py#L332-L342
+ _set_crackfortran(crackfortran_setts)
+ # Step 6.3: 1. https://github.com/numpy/numpy/blob/45bc13e6d922690eea43b9d807d476e0f243f836/numpy/f2py/f2py2e.py#L440
+ # 2. https://github.com/numpy/numpy/blob/main/numpy/f2py/f2py2e.py#L247-L248
+ _set_capi_maps(capi_maps_setts)
+ # Step 6.4: 1. https://github.com/numpy/numpy/blob/45bc13e6d922690eea43b9d807d476e0f243f836/numpy/f2py/f2py2e.py#L439
+ # 2. https://github.com/numpy/numpy/blob/main/numpy/f2py/f2py2e.py#L471-L473
+ _set_auxfuncs(auxfuncs_setts)
+
+def generate_files(files: list[str], module_name: str, sign_file: Path) -> list[Path]:
+ """Generate signature file if wanted and return list of wrappers to be compiled"""
+ # Step 8.1: Generate postlist from crackfortran
+ postlist = _callcrackfortran(files, module_name)
+
+ # Step 8.2: Check postlist. This function is taken from the following code:
+ # https://github.com/numpy/numpy/blob/main/numpy/f2py/f2py2e.py#L443-L456
+ _check_postlist(postlist, sign_file)
+ if(sign_file):
+ # Step 8.3: Generate signature file, take from this code piece
+ # https://github.com/numpy/numpy/blob/main/numpy/f2py/f2py2e.py#L343-L350
+ _generate_signature(postlist, sign_file)
+ return
+ # Step 8.4: Same as the buildmodules folder of f2py2e
+ ret = _buildmodules(postlist)
+ module_name = list(ret.keys())[0]
+ wrappers = []
+ wrappers.extend(ret.get(module_name).get('csrc', []))
+ wrappers.extend(ret.get(module_name).get('fsrc', []))
+ return [Path(wrapper) for wrapper in wrappers]
+
+def compile_dist(ext_args: dict[str, Any], link_resources: list[str], build_dir: Path, fc_flags: list[str], flib_flags: list[str], quiet_build: bool) -> None:
+ # Step 7.2: The entire code below mimics 'f2py2e:run_compile()'
+ # https://github.com/numpy/numpy/blob/main/numpy/f2py/f2py2e.py#L647-L669
+ _set_dependencies_dist(ext_args, link_resources)
+ f2py_dir = get_f2py_dir()
+ ext = Extension(**ext_args)
+ f2py_build_flags = ['--quiet'] if quiet_build else ['--verbose']
+ f2py_build_flags.extend( ['build', '--build-temp', str(build_dir),
+ '--build-base', str(build_dir),
+ '--build-platlib', '.',
+ '--disable-optimization'])
+ if fc_flags:
+ f2py_build_flags.extend(['config_fc'] + fc_flags)
+ if flib_flags:
+ f2py_build_flags.extend(['build_ext'] + flib_flags)
+
+ # f2py2e used to pass `script_name` and `script_args` through `sys.argv` array
+ # Now we are passing it as attributes. They will be read later distutils core
+ # https://github.com/pypa/distutils/blob/main/distutils/core.py#L131-L134
+ setup(ext_modules=[ext], script_name=f2py_dir, script_args=f2py_build_flags)
+
+def segregate_files(files: list[str]) -> tuple[list[str], list[str], list[str], list[str], list[str]]:
+ """
+ Segregate files into five groups:
+ * Fortran 77 files
+ * Fortran 90 and above files
+ * F2PY Signature files
+ * Object files
+ * others
+ """
+ f77_ext = ('.f', '.for', '.ftn', '.f77')
+ f90_ext = ('.f90', '.f95', '.f03', '.f08')
+ pyf_ext = ('.pyf', '.src')
+ out_ext = ('.o', '.out', '.so', '.a')
+
+ f77_files = []
+ f90_files = []
+ out_files = []
+ pyf_files = []
+ other_files = []
+
+ for f in files:
+ f_path = PurePath(f)
+ ext = f_path.suffix
+ if ext in f77_ext:
+ f77_files.append(f)
+ elif ext in f90_ext:
+ f90_files.append(f)
+ elif ext in out_ext:
+ out_files.append(f)
+ elif ext in pyf_ext:
+ if ext == '.src' and f_path.stem.endswith('.pyf') or ext != '.src':
+ pyf_files.append(f)
+ else:
+ other_files.append(f)
+
+ return f77_files, f90_files, pyf_files, out_files, other_files
\ No newline at end of file
diff --git a/numpy/f2py/setup.py b/numpy/f2py/setup.py
index 499609f96600..cc4c19c08fb6 100644
--- a/numpy/f2py/setup.py
+++ b/numpy/f2py/setup.py
@@ -26,6 +26,9 @@
def configuration(parent_package='', top_path=None):
config = Configuration('f2py', parent_package, top_path)
config.add_subpackage('tests')
+ config.add_subpackage('backends')
+ config.add_data_dir('backends/src')
+ config.add_data_files('backends/src/meson.build.src')
config.add_data_dir('tests/src')
config.add_data_files(
'src/fortranobject.c',
diff --git a/numpy/f2py/utils.py b/numpy/f2py/utils.py
new file mode 100644
index 000000000000..95c7ef12ad9c
--- /dev/null
+++ b/numpy/f2py/utils.py
@@ -0,0 +1,37 @@
+"""Global f2py utilities."""
+
+from __future__ import annotations
+
+import contextlib
+import tempfile
+import shutil
+
+from typing import Optional
+from pathlib import Path
+
+def get_f2py_dir() -> Path:
+ """Return the directory where f2py is installed."""
+ return Path(__file__).resolve().parent
+
+@contextlib.contextmanager
+def open_build_dir(build_dir: Optional[list[str]], compile: bool) -> Path:
+ """Create build directory if the user specifies it,
+ Otherwise, create a temporary directory and remove it.
+
+ Default build directory for only wrapper generation is
+ the current directory. Therefore, if `compile` is False,
+ the wrappers are generated in the current directory"""
+
+ remove_build_dir: bool = False
+ if(isinstance(build_dir, list)):
+ build_dir = build_dir[0] if build_dir else None
+ if build_dir is None:
+ if compile:
+ remove_build_dir = True
+ build_dir = Path(tempfile.mkdtemp())
+ else:
+ build_dir = Path.cwd()
+ else:
+ build_dir = Path(build_dir).absolute()
+ yield build_dir
+ shutil.rmtree(build_dir) if remove_build_dir else None
\ No newline at end of file
diff --git a/numpy/tests/test_public_api.py b/numpy/tests/test_public_api.py
index 882a48d2ff4e..63ce4e865007 100644
--- a/numpy/tests/test_public_api.py
+++ b/numpy/tests/test_public_api.py
@@ -246,6 +246,9 @@ def test_NPY_NO_EXPORT():
"distutils.unixccompiler",
"dual",
"f2py.auxfuncs",
+ "f2py.backends",
+ "f2py.backends.backend",
+ "f2py.backends.meson_backend",
"f2py.capi_maps",
"f2py.cb_rules",
"f2py.cfuncs",
@@ -253,11 +256,14 @@ def test_NPY_NO_EXPORT():
"f2py.crackfortran",
"f2py.diagnose",
"f2py.f2py2e",
+ "f2py.f2pyarg",
"f2py.f90mod_rules",
"f2py.func2subr",
"f2py.rules",
"f2py.symbolic",
"f2py.use_rules",
+ "f2py.service",
+ "f2py.utils",
"fft.helper",
"lib.arraypad",
"lib.arraysetops",
diff --git a/setup.py b/setup.py
index 3aa7504b05cb..db637b4f406a 100755
--- a/setup.py
+++ b/setup.py
@@ -420,13 +420,13 @@ def setup_package():
# The f2py scripts that will be installed
if sys.platform == 'win32':
f2py_cmds = [
- 'f2py = numpy.f2py.f2py2e:main',
+ 'f2py = numpy.f2py.f2pyarg:main',
]
else:
f2py_cmds = [
- 'f2py = numpy.f2py.f2py2e:main',
- 'f2py%s = numpy.f2py.f2py2e:main' % sys.version_info[:1],
- 'f2py%s.%s = numpy.f2py.f2py2e:main' % sys.version_info[:2],
+ 'f2py = numpy.f2py.f2pyarg:main',
+ 'f2py%s = numpy.f2py.f2pyarg:main' % sys.version_info[:1],
+ 'f2py%s.%s = numpy.f2py.f2pyarg:main' % sys.version_info[:2],
]
cmdclass["sdist"] = sdist_checked