Skip to content

BUG: Disallow shadowed modulenames #25205

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Nov 20, 2023
4 changes: 4 additions & 0 deletions doc/release/upcoming_changes/25181.compatibility.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
``f2py`` will no longer accept ambiguous ``-m`` and ``.pyf`` CLI combinations.
When more than one ``.pyf`` file is passed, an error is raised. When both ``-m``
and a ``.pyf`` is passed, a warning is emitted and the ``-m`` provided name is
ignored.
50 changes: 27 additions & 23 deletions doc/source/f2py/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -208,29 +208,33 @@ Other options
``-m <modulename>``
Name of an extension module. Default is ``untitled``.

.. warning:: Don't use this option if a signature file (``*.pyf``) is used.

``--[no-]lower``
Do [not] lower the cases in ``<fortran files>``. By default, ``--lower`` is
assumed with ``-h`` switch, and ``--no-lower`` without the ``-h`` switch.
``-include<header>``
Writes additional headers in the C wrapper, can be passed multiple times,
generates #include <header> each time. Note that this is meant to be passed
in single quotes and without spaces, for example ``'-include<stdbool.h>'``
``--build-dir <dirname>``
All F2PY generated files are created in ``<dirname>``. Default is
``tempfile.mkdtemp()``.
``--f2cmap <filename>``
Load Fortran-to-C ``KIND`` specifications from the given file.
``--quiet``
Run quietly.
``--verbose``
Run with extra verbosity.
``--skip-empty-wrappers``
Do not generate wrapper files unless required by the inputs.
This is a backwards compatibility flag to restore pre 1.22.4 behavior.
``-v``
Print the F2PY version and exit.
.. warning::
Don't use this option if a signature file (``*.pyf``) is used.

.. versionchanged:: 1.26.3
Will ignore ``-m`` if a ``pyf`` file is provided.

``--[no-]lower``
Do [not] lower the cases in ``<fortran files>``. By default, ``--lower`` is
assumed with ``-h`` switch, and ``--no-lower`` without the ``-h`` switch.
``-include<header>``
Writes additional headers in the C wrapper, can be passed multiple times,
generates #include <header> each time. Note that this is meant to be passed
in single quotes and without spaces, for example ``'-include<stdbool.h>'``
``--build-dir <dirname>``
All F2PY generated files are created in ``<dirname>``. Default is
``tempfile.mkdtemp()``.
``--f2cmap <filename>``
Load Fortran-to-C ``KIND`` specifications from the given file.
``--quiet``
Run quietly.
``--verbose``
Run with extra verbosity.
``--skip-empty-wrappers``
Do not generate wrapper files unless required by the inputs.
This is a backwards compatibility flag to restore pre 1.22.4 behavior.
``-v``
Print the F2PY version and exit.

Execute ``f2py`` without any options to get an up-to-date list of available
options.
Expand Down
64 changes: 42 additions & 22 deletions numpy/f2py/f2py2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,16 @@ def run_main(comline_list):
f2pydir = os.path.dirname(os.path.abspath(cfuncs.__file__))
fobjhsrc = os.path.join(f2pydir, 'src', 'fortranobject.h')
fobjcsrc = os.path.join(f2pydir, 'src', 'fortranobject.c')
# gh-22819 -- begin
parser = make_f2py_parser()
args, comline_list = parser.parse_known_args(comline_list)
pyf_files, _ = filter_files("", "[.]pyf([.]src|)", comline_list)
# Checks that no existing modulename is defined in a pyf file
# TODO: Remove all this when scaninputline is replaced
if "-h" not in comline_list and args.module_name: # Can't check what doesn't exist yet, -h creates the pyf
modname = validate_modulename(pyf_files, args.module_name)
comline_list += ['-m', modname] # needed for the rest of scaninputline
# gh-22819 -- end
files, options = scaninputline(comline_list)
auxfuncs.options = options
capi_maps.load_f2cmap_file(options['f2cmap_file'])
Expand Down Expand Up @@ -522,24 +532,30 @@ def get_prefix(module):
p = os.path.dirname(os.path.dirname(module.__file__))
return p

def preparse_sysargv():
# To keep backwards bug compatibility, newer flags are handled by argparse,
# and `sys.argv` is passed to the rest of `f2py` as is.
def make_f2py_parser():
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("--dep", action="append", dest="dependencies")
parser.add_argument("--backend", choices=['meson', 'distutils'], default='distutils')
parser.add_argument("-m", dest="module_name")
return parser

def preparse_sysargv():
# To keep backwards bug compatibility, newer flags are handled by argparse,
# and `sys.argv` is passed to the rest of `f2py` as is.
parser = make_f2py_parser()

args, remaining_argv = parser.parse_known_args()
sys.argv = [sys.argv[0]] + remaining_argv

backend_key = args.backend
if sys.version_info >= (3, 12) and backend_key == 'distutils':
outmess('Cannot use distutils backend with Python 3.12, using meson backend instead.')
outmess("Cannot use distutils backend with Python 3.12, using meson backend instead.\n")
backend_key = 'meson'

return {
"dependencies": args.dependencies or [],
"backend": backend_key
"backend": backend_key,
"modulename": args.module_name,
}

def run_compile():
Expand All @@ -550,11 +566,13 @@ def run_compile():

# Collect dependency flags, preprocess sys.argv
argy = preparse_sysargv()
modulename = argy["modulename"]
if modulename is None:
modulename = 'untitled'
dependencies = argy["dependencies"]
backend_key = argy["backend"]
build_backend = f2py_build_generator(backend_key)


i = sys.argv.index('-c')
del sys.argv[i]

Expand Down Expand Up @@ -634,30 +652,17 @@ def run_compile():
if '--quiet' in f2py_flags:
setup_flags.append('--quiet')

modulename = 'untitled'
sources = sys.argv[1:]

for optname in ['--include_paths', '--include-paths', '--f2cmap']:
if optname in sys.argv:
i = sys.argv.index(optname)
f2py_flags.extend(sys.argv[i:i + 2])
del sys.argv[i + 1], sys.argv[i]
sources = sys.argv[1:]

pyf_files = []
if '-m' in sys.argv:
i = sys.argv.index('-m')
modulename = sys.argv[i + 1]
del sys.argv[i + 1], sys.argv[i]
sources = sys.argv[1:]
else:
pyf_files, _sources = filter_files('', '[.]pyf([.]src|)', sources)
sources = pyf_files + _sources
for f in pyf_files:
modulename = auxfuncs.get_f2py_modulename(f)
if modulename:
break

pyf_files, _sources = filter_files("", "[.]pyf([.]src|)", sources)
sources = pyf_files + _sources
modulename = validate_modulename(pyf_files, modulename)
extra_objects, sources = filter_files('', '[.](o|a|so|dylib)', sources)
include_dirs, sources = filter_files('-I', '', sources, remove_prefix=1)
library_dirs, sources = filter_files('-L', '', sources, remove_prefix=1)
Expand Down Expand Up @@ -704,6 +709,21 @@ def run_compile():

builder.compile()


def validate_modulename(pyf_files, modulename='untitled'):
if len(pyf_files) > 1:
raise ValueError("Only one .pyf file per call")
if pyf_files:
pyff = pyf_files[0]
pyf_modname = auxfuncs.get_f2py_modulename(pyff)
if modulename != pyf_modname:
outmess(
f"Ignoring -m {modulename}.\n"
f"{pyff} defines {pyf_modname} to be the modulename.\n"
)
modulename = pyf_modname
return modulename

def main():
if '--help-link' in sys.argv[1:]:
sys.argv.remove('--help-link')
Expand Down
2 changes: 1 addition & 1 deletion numpy/f2py/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def configuration(parent_package='', top_path=None):
config.add_data_files(
'src/fortranobject.c',
'src/fortranobject.h',
'backends/meson.build.template',
'_backends/meson.build.template',
)
config.add_data_files('*.pyi')
return config
Expand Down
6 changes: 6 additions & 0 deletions numpy/f2py/tests/src/cli/gh_22819.pyf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
python module test_22819
interface
subroutine hello()
end subroutine hello
end interface
end python module test_22819
56 changes: 56 additions & 0 deletions numpy/f2py/tests/test_f2py2e.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import textwrap, re, sys, subprocess, shlex
from pathlib import Path
from collections import namedtuple
import platform

import pytest

Expand Down Expand Up @@ -71,6 +72,15 @@ def gh23598_warn(tmpdir_factory):
return fn


@pytest.fixture(scope="session")
def gh22819_cli(tmpdir_factory):
"""F90 file for testing disallowed CLI arguments in ghff819"""
fdat = util.getpath("tests", "src", "cli", "gh_22819.pyf").read_text()
fn = tmpdir_factory.getbasetemp() / "gh_22819.pyf"
fn.write_text(fdat, encoding="ascii")
return fn


@pytest.fixture(scope="session")
def hello_world_f77(tmpdir_factory):
"""Generates a single f77 file for testing"""
Expand Down Expand Up @@ -100,6 +110,38 @@ def f2cmap_f90(tmpdir_factory):
return fn


def test_gh22819_cli(capfd, gh22819_cli, monkeypatch):
"""Check that module names are handled correctly
gh-22819
Essentially, the -m name cannot be used to import the module, so the module
named in the .pyf needs to be used instead

CLI :: -m and a .pyf file
"""
ipath = Path(gh22819_cli)
monkeypatch.setattr(sys, "argv", f"f2py -m blah {ipath}".split())
with util.switchdir(ipath.parent):
f2pycli()
gen_paths = [item.name for item in ipath.parent.rglob("*") if item.is_file()]
assert "blahmodule.c" not in gen_paths # shouldn't be generated
assert "blah-f2pywrappers.f" not in gen_paths
assert "test_22819-f2pywrappers.f" in gen_paths
assert "test_22819module.c" in gen_paths
assert "Ignoring blah"


def test_gh22819_many_pyf(capfd, gh22819_cli, monkeypatch):
"""Only one .pyf file allowed
gh-22819
CLI :: .pyf files
"""
ipath = Path(gh22819_cli)
monkeypatch.setattr(sys, "argv", f"f2py -m blah {ipath} hello.pyf".split())
with util.switchdir(ipath.parent):
with pytest.raises(ValueError, match="Only one .pyf file per call"):
f2pycli()


def test_gh23598_warn(capfd, gh23598_warn, monkeypatch):
foutl = get_io_paths(gh23598_warn, mname="test")
ipath = foutl.f90inp
Expand Down Expand Up @@ -156,6 +198,20 @@ def test_gen_pyf_no_overwrite(capfd, hello_world_f90, monkeypatch):
assert "Use --overwrite-signature to overwrite" in err


@pytest.mark.skipif((platform.system() != 'Linux') and (sys.version_info <= (3, 12)), reason='Compiler and 3.12 required')
def test_untitled_cli(capfd, hello_world_f90, monkeypatch):
"""Check that modules are named correctly

CLI :: defaults
"""
ipath = Path(hello_world_f90)
monkeypatch.setattr(sys, "argv", f"f2py --backend meson -c {ipath}".split())
with util.switchdir(ipath.parent):
f2pycli()
out, _ = capfd.readouterr()
assert "untitledmodule.c" in out


@pytest.mark.xfail
def test_f2py_skip(capfd, retreal_f77, monkeypatch):
"""Tests that functions can be skipped
Expand Down