diff --git a/numpy/distutils/command/build_src.py b/numpy/distutils/command/build_src.py index bf3d03c70e44..7303db124cc8 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 dbe3df27f6ec..7856eb601e0e 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, capture_output=True) except OSError: 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/capi_maps.py b/numpy/f2py/capi_maps.py index c7efe87e82ba..da5a639ffcea 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 @@ -141,9 +141,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 @@ -151,8 +152,8 @@ 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)) - with open(f2cmap_file) as f: + 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(): for k1 in d1.keys(): @@ -245,7 +246,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 86d7004abad4..459d5797e9a2 100644 --- a/numpy/f2py/diagnose.py +++ b/numpy/f2py/diagnose.py @@ -29,17 +29,17 @@ def run(): try: import numpy - has_newnumpy = 1 - except ImportError as e: - print('Failed to import new numpy:', e) - has_newnumpy = 0 + has_newnumpy = True + except ImportError: + print('Failed to import new numpy:', sys.exc_info()[1]) + has_newnumpy = False try: - from numpy.f2py import f2py2e - has_f2py2e = 1 - except ImportError as e: - print('Failed to import f2py2e:', e) - has_f2py2e = 0 + from numpy.f2py import f2pyarg + has_f2pyarg = True + except ImportError: + 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..11f7b351c0aa --- /dev/null +++ b/numpy/f2py/f2pyarg.py @@ -0,0 +1,844 @@ +#!/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 + +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 + +################## +# 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 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) + +########## +# 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", + 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="*", + 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" +) + + +# 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): + 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': args.include_dirs, + 'library_dirs': 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 + generate_files(f77_files + f90_files, module_name, sign_file) + +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..acbfafda0f87 --- /dev/null +++ b/numpy/f2py/service.py @@ -0,0 +1,345 @@ +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]) -> None: + # 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) -> None: + # 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 + if(module_name): + # Step 8.4: Same as the buildmodules folder of f2py2e + _buildmodules(postlist) + +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/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 9e802e756d2f..2c5d7435a4af 100644 --- a/numpy/tests/test_public_api.py +++ b/numpy/tests/test_public_api.py @@ -197,11 +197,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 6bd2153d7835..be2a6c7d909f 100755 --- a/setup.py +++ b/setup.py @@ -491,13 +491,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], ] metadata = dict(