diff --git a/numpy/f2py/_backends/_meson.py b/numpy/f2py/_backends/_meson.py
index 8691af6ee517..01e356d68e4b 100644
--- a/numpy/f2py/_backends/_meson.py
+++ b/numpy/f2py/_backends/_meson.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import os
import errno
import shutil
import subprocess
@@ -88,8 +89,14 @@ def _move_exec_to_root(self, build_dir: Path):
walk_dir.glob(f"{self.modulename}*.so"),
walk_dir.glob(f"{self.modulename}*.pyd"),
)
+ # Same behavior as distutils
+ # https://github.com/numpy/numpy/issues/24874#issuecomment-1835632293
for path_object in path_objects:
- shutil.move(path_object, Path.cwd())
+ dest_path = Path.cwd() / path_object.name
+ if dest_path.exists():
+ dest_path.unlink()
+ shutil.copy2(path_object, dest_path)
+ os.remove(path_object)
def write_meson_build(self, build_dir: Path) -> None:
"""Writes the meson build file at specified location"""
diff --git a/numpy/f2py/_src_pyf.py b/numpy/f2py/_src_pyf.py
new file mode 100644
index 000000000000..6247b95bfe46
--- /dev/null
+++ b/numpy/f2py/_src_pyf.py
@@ -0,0 +1,239 @@
+import re
+
+# START OF CODE VENDORED FROM `numpy.distutils.from_template`
+#############################################################
+"""
+process_file(filename)
+
+ takes templated file .xxx.src and produces .xxx file where .xxx
+ is .pyf .f90 or .f using the following template rules:
+
+ '<..>' denotes a template.
+
+ All function and subroutine blocks in a source file with names that
+ contain '<..>' will be replicated according to the rules in '<..>'.
+
+ The number of comma-separated words in '<..>' will determine the number of
+ replicates.
+
+ '<..>' may have two different forms, named and short. For example,
+
+ named:
+
where anywhere inside a block '
' will be replaced with
+ 'd', 's', 'z', and 'c' for each replicate of the block.
+
+ <_c> is already defined: <_c=s,d,c,z>
+ <_t> is already defined: <_t=real,double precision,complex,double complex>
+
+ short:
+ , a short form of the named, useful when no
appears inside
+ a block.
+
+ In general, '<..>' contains a comma separated list of arbitrary
+ expressions. If these expression must contain a comma|leftarrow|rightarrow,
+ then prepend the comma|leftarrow|rightarrow with a backslash.
+
+ If an expression matches '\\' then it will be replaced
+ by -th expression.
+
+ Note that all '<..>' forms in a block must have the same number of
+ comma-separated entries.
+
+ Predefined named template rules:
+
+
+
+
+
+"""
+
+routine_start_re = re.compile(r'(\n|\A)(( (\$|\*))|)\s*(subroutine|function)\b', re.I)
+routine_end_re = re.compile(r'\n\s*end\s*(subroutine|function)\b.*(\n|\Z)', re.I)
+function_start_re = re.compile(r'\n (\$|\*)\s*function\b', re.I)
+
+def parse_structure(astr):
+ """ Return a list of tuples for each function or subroutine each
+ tuple is the start and end of a subroutine or function to be
+ expanded.
+ """
+
+ spanlist = []
+ ind = 0
+ while True:
+ m = routine_start_re.search(astr, ind)
+ if m is None:
+ break
+ start = m.start()
+ if function_start_re.match(astr, start, m.end()):
+ while True:
+ i = astr.rfind('\n', ind, start)
+ if i==-1:
+ break
+ start = i
+ if astr[i:i+7]!='\n $':
+ break
+ start += 1
+ m = routine_end_re.search(astr, m.end())
+ ind = end = m and m.end()-1 or len(astr)
+ spanlist.append((start, end))
+ return spanlist
+
+template_re = re.compile(r"<\s*(\w[\w\d]*)\s*>")
+named_re = re.compile(r"<\s*(\w[\w\d]*)\s*=\s*(.*?)\s*>")
+list_re = re.compile(r"<\s*((.*?))\s*>")
+
+def find_repl_patterns(astr):
+ reps = named_re.findall(astr)
+ names = {}
+ for rep in reps:
+ name = rep[0].strip() or unique_key(names)
+ repl = rep[1].replace(r'\,', '@comma@')
+ thelist = conv(repl)
+ names[name] = thelist
+ return names
+
+def find_and_remove_repl_patterns(astr):
+ names = find_repl_patterns(astr)
+ astr = re.subn(named_re, '', astr)[0]
+ return astr, names
+
+item_re = re.compile(r"\A\\(?P\d+)\Z")
+def conv(astr):
+ b = astr.split(',')
+ l = [x.strip() for x in b]
+ for i in range(len(l)):
+ m = item_re.match(l[i])
+ if m:
+ j = int(m.group('index'))
+ l[i] = l[j]
+ return ','.join(l)
+
+def unique_key(adict):
+ """ Obtain a unique key given a dictionary."""
+ allkeys = list(adict.keys())
+ done = False
+ n = 1
+ while not done:
+ newkey = '__l%s' % (n)
+ if newkey in allkeys:
+ n += 1
+ else:
+ done = True
+ return newkey
+
+
+template_name_re = re.compile(r'\A\s*(\w[\w\d]*)\s*\Z')
+def expand_sub(substr, names):
+ substr = substr.replace(r'\>', '@rightarrow@')
+ substr = substr.replace(r'\<', '@leftarrow@')
+ lnames = find_repl_patterns(substr)
+ substr = named_re.sub(r"<\1>", substr) # get rid of definition templates
+
+ def listrepl(mobj):
+ thelist = conv(mobj.group(1).replace(r'\,', '@comma@'))
+ if template_name_re.match(thelist):
+ return "<%s>" % (thelist)
+ name = None
+ for key in lnames.keys(): # see if list is already in dictionary
+ if lnames[key] == thelist:
+ name = key
+ if name is None: # this list is not in the dictionary yet
+ name = unique_key(lnames)
+ lnames[name] = thelist
+ return "<%s>" % name
+
+ substr = list_re.sub(listrepl, substr) # convert all lists to named templates
+ # newnames are constructed as needed
+
+ numsubs = None
+ base_rule = None
+ rules = {}
+ for r in template_re.findall(substr):
+ if r not in rules:
+ thelist = lnames.get(r, names.get(r, None))
+ if thelist is None:
+ raise ValueError('No replicates found for <%s>' % (r))
+ if r not in names and not thelist.startswith('_'):
+ names[r] = thelist
+ rule = [i.replace('@comma@', ',') for i in thelist.split(',')]
+ num = len(rule)
+
+ if numsubs is None:
+ numsubs = num
+ rules[r] = rule
+ base_rule = r
+ elif num == numsubs:
+ rules[r] = rule
+ else:
+ print("Mismatch in number of replacements (base <{}={}>) "
+ "for <{}={}>. Ignoring.".format(base_rule, ','.join(rules[base_rule]), r, thelist))
+ if not rules:
+ return substr
+
+ def namerepl(mobj):
+ name = mobj.group(1)
+ return rules.get(name, (k+1)*[name])[k]
+
+ newstr = ''
+ for k in range(numsubs):
+ newstr += template_re.sub(namerepl, substr) + '\n\n'
+
+ newstr = newstr.replace('@rightarrow@', '>')
+ newstr = newstr.replace('@leftarrow@', '<')
+ return newstr
+
+def process_str(allstr):
+ newstr = allstr
+ writestr = ''
+
+ struct = parse_structure(newstr)
+
+ oldend = 0
+ names = {}
+ names.update(_special_names)
+ for sub in struct:
+ cleanedstr, defs = find_and_remove_repl_patterns(newstr[oldend:sub[0]])
+ writestr += cleanedstr
+ names.update(defs)
+ writestr += expand_sub(newstr[sub[0]:sub[1]], names)
+ oldend = sub[1]
+ writestr += newstr[oldend:]
+
+ return writestr
+
+include_src_re = re.compile(r"(\n|\A)\s*include\s*['\"](?P[\w\d./\\]+\.src)['\"]", re.I)
+
+def resolve_includes(source):
+ d = os.path.dirname(source)
+ with open(source) as fid:
+ lines = []
+ for line in fid:
+ m = include_src_re.match(line)
+ if m:
+ fn = m.group('name')
+ if not os.path.isabs(fn):
+ fn = os.path.join(d, fn)
+ if os.path.isfile(fn):
+ lines.extend(resolve_includes(fn))
+ else:
+ lines.append(line)
+ else:
+ lines.append(line)
+ return lines
+
+def process_file(source):
+ lines = resolve_includes(source)
+ return process_str(''.join(lines))
+
+_special_names = find_repl_patterns('''
+<_c=s,d,c,z>
+<_t=real,double precision,complex,double complex>
+
+
+
+
+
+''')
+
+# END OF CODE VENDORED FROM `numpy.distutils.from_template`
+###########################################################
diff --git a/numpy/f2py/cfuncs.py b/numpy/f2py/cfuncs.py
index 7a5f4c1d709a..15556d7f89ee 100644
--- a/numpy/f2py/cfuncs.py
+++ b/numpy/f2py/cfuncs.py
@@ -250,14 +250,19 @@
#define slen f2py_slen
#define size f2py_size
"""
-cppmacros[
- 'pyobj_from_char1'] = '#define pyobj_from_char1(v) (PyLong_FromLong(v))'
-cppmacros[
- 'pyobj_from_short1'] = '#define pyobj_from_short1(v) (PyLong_FromLong(v))'
+cppmacros['pyobj_from_char1'] = r"""
+#define pyobj_from_char1(v) (PyLong_FromLong(v))
+"""
+cppmacros['pyobj_from_short1'] = r"""
+#define pyobj_from_short1(v) (PyLong_FromLong(v))
+"""
needs['pyobj_from_int1'] = ['signed_char']
-cppmacros['pyobj_from_int1'] = '#define pyobj_from_int1(v) (PyLong_FromLong(v))'
-cppmacros[
- 'pyobj_from_long1'] = '#define pyobj_from_long1(v) (PyLong_FromLong(v))'
+cppmacros['pyobj_from_int1'] = r"""
+#define pyobj_from_int1(v) (PyLong_FromLong(v))
+"""
+cppmacros['pyobj_from_long1'] = r"""
+#define pyobj_from_long1(v) (PyLong_FromLong(v))
+"""
needs['pyobj_from_long_long1'] = ['long_long']
cppmacros['pyobj_from_long_long1'] = """
#ifdef HAVE_LONG_LONG
@@ -268,27 +273,27 @@
#endif
"""
needs['pyobj_from_long_double1'] = ['long_double']
-cppmacros[
- 'pyobj_from_long_double1'] = '#define pyobj_from_long_double1(v) (PyFloat_FromDouble(v))'
-cppmacros[
- 'pyobj_from_double1'] = '#define pyobj_from_double1(v) (PyFloat_FromDouble(v))'
-cppmacros[
- 'pyobj_from_float1'] = '#define pyobj_from_float1(v) (PyFloat_FromDouble(v))'
+cppmacros['pyobj_from_long_double1'] = """
+#define pyobj_from_long_double1(v) (PyFloat_FromDouble(v))"""
+cppmacros['pyobj_from_double1'] = """
+#define pyobj_from_double1(v) (PyFloat_FromDouble(v))"""
+cppmacros['pyobj_from_float1'] = """
+#define pyobj_from_float1(v) (PyFloat_FromDouble(v))"""
needs['pyobj_from_complex_long_double1'] = ['complex_long_double']
-cppmacros[
- 'pyobj_from_complex_long_double1'] = '#define pyobj_from_complex_long_double1(v) (PyComplex_FromDoubles(v.r,v.i))'
+cppmacros['pyobj_from_complex_long_double1'] = """
+#define pyobj_from_complex_long_double1(v) (PyComplex_FromDoubles(v.r,v.i))"""
needs['pyobj_from_complex_double1'] = ['complex_double']
-cppmacros[
- 'pyobj_from_complex_double1'] = '#define pyobj_from_complex_double1(v) (PyComplex_FromDoubles(v.r,v.i))'
+cppmacros['pyobj_from_complex_double1'] = """
+#define pyobj_from_complex_double1(v) (PyComplex_FromDoubles(v.r,v.i))"""
needs['pyobj_from_complex_float1'] = ['complex_float']
-cppmacros[
- 'pyobj_from_complex_float1'] = '#define pyobj_from_complex_float1(v) (PyComplex_FromDoubles(v.r,v.i))'
+cppmacros['pyobj_from_complex_float1'] = """
+#define pyobj_from_complex_float1(v) (PyComplex_FromDoubles(v.r,v.i))"""
needs['pyobj_from_string1'] = ['string']
-cppmacros[
- 'pyobj_from_string1'] = '#define pyobj_from_string1(v) (PyUnicode_FromString((char *)v))'
+cppmacros['pyobj_from_string1'] = """
+#define pyobj_from_string1(v) (PyUnicode_FromString((char *)v))"""
needs['pyobj_from_string1size'] = ['string']
-cppmacros[
- 'pyobj_from_string1size'] = '#define pyobj_from_string1size(v,len) (PyUnicode_FromStringAndSize((char *)v, len))'
+cppmacros['pyobj_from_string1size'] = """
+#define pyobj_from_string1size(v,len) (PyUnicode_FromStringAndSize((char *)v, len))"""
needs['TRYPYARRAYTEMPLATE'] = ['PRINTPYOBJERR']
cppmacros['TRYPYARRAYTEMPLATE'] = """
/* New SciPy */
diff --git a/numpy/f2py/f2py2e.py b/numpy/f2py/f2py2e.py
index 07383a2ba4ce..1a24e056e593 100755
--- a/numpy/f2py/f2py2e.py
+++ b/numpy/f2py/f2py2e.py
@@ -455,7 +455,6 @@ def run_main(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
- modname = "untitled" # Default
if args.module_name:
if "-h" in comline_list:
modname = (
@@ -465,7 +464,7 @@ def run_main(comline_list):
modname = validate_modulename(
pyf_files, args.module_name
) # Validate modname when -h is not present
- comline_list += ['-m', modname] # needed for the rest of scaninputline
+ comline_list += ['-m', modname] # needed for the rest of scaninputline
# gh-22819 -- end
files, options = scaninputline(comline_list)
auxfuncs.options = options
@@ -688,12 +687,12 @@ def run_compile():
# Construct wrappers / signatures / things
if backend_key == 'meson':
- outmess('Using meson backend\nWill pass --lower to f2py\nSee https://numpy.org/doc/stable/f2py/buildtools/meson.html\n')
- f2py_flags.append('--lower')
- if pyf_files:
- run_main(f" {' '.join(f2py_flags)} {' '.join(pyf_files)}".split())
- else:
+ if not pyf_files:
+ outmess('Using meson backend\nWill pass --lower to f2py\nSee https://numpy.org/doc/stable/f2py/buildtools/meson.html\n')
+ f2py_flags.append('--lower')
run_main(f" {' '.join(f2py_flags)} -m {modulename} {' '.join(sources)}".split())
+ else:
+ run_main(f" {' '.join(f2py_flags)} {' '.join(pyf_files)}".split())
# Now use the builder
builder = build_backend(
diff --git a/numpy/f2py/tests/src/string/gh25286.f90 b/numpy/f2py/tests/src/string/gh25286.f90
new file mode 100644
index 000000000000..db1c7100d2ab
--- /dev/null
+++ b/numpy/f2py/tests/src/string/gh25286.f90
@@ -0,0 +1,14 @@
+subroutine charint(trans, info)
+ character, intent(in) :: trans
+ integer, intent(out) :: info
+ if (trans == 'N') then
+ info = 1
+ else if (trans == 'T') then
+ info = 2
+ else if (trans == 'C') then
+ info = 3
+ else
+ info = -1
+ end if
+
+end subroutine charint
diff --git a/numpy/f2py/tests/src/string/gh25286.pyf b/numpy/f2py/tests/src/string/gh25286.pyf
new file mode 100644
index 000000000000..7b9609071bce
--- /dev/null
+++ b/numpy/f2py/tests/src/string/gh25286.pyf
@@ -0,0 +1,12 @@
+python module _char_handling_test
+ interface
+ subroutine charint(trans, info)
+ callstatement (*f2py_func)(&trans, &info)
+ callprotoargument char*, int*
+
+ character, intent(in), check(trans=='N'||trans=='T'||trans=='C') :: trans = 'N'
+ integer intent(out) :: info
+
+ end subroutine charint
+ end interface
+end python module _char_handling_test
diff --git a/numpy/f2py/tests/src/string/gh25286_bc.pyf b/numpy/f2py/tests/src/string/gh25286_bc.pyf
new file mode 100644
index 000000000000..e7b10fa9215e
--- /dev/null
+++ b/numpy/f2py/tests/src/string/gh25286_bc.pyf
@@ -0,0 +1,12 @@
+python module _char_handling_test
+ interface
+ subroutine charint(trans, info)
+ callstatement (*f2py_func)(&trans, &info)
+ callprotoargument char*, int*
+
+ character, intent(in), check(*trans=='N'||*trans=='T'||*trans=='C') :: trans = 'N'
+ integer intent(out) :: info
+
+ end subroutine charint
+ end interface
+end python module _char_handling_test
diff --git a/numpy/f2py/tests/test_character.py b/numpy/f2py/tests/test_character.py
index 078f5fcd3925..50e55e1a91cf 100644
--- a/numpy/f2py/tests/test_character.py
+++ b/numpy/f2py/tests/test_character.py
@@ -610,3 +610,30 @@ def test_gh24662(self):
with pytest.raises(Exception):
aa = "Hi"
self.module.string_inout_optional(aa)
+
+
+@pytest.mark.slow
+class TestNewCharHandling(util.F2PyTest):
+ # from v1.24 onwards, gh-19388
+ sources = [
+ util.getpath("tests", "src", "string", "gh25286.pyf"),
+ util.getpath("tests", "src", "string", "gh25286.f90")
+ ]
+ module_name = "_char_handling_test"
+
+ def test_gh25286(self):
+ info = self.module.charint('T')
+ assert info == 2
+
+@pytest.mark.slow
+class TestBCCharHandling(util.F2PyTest):
+ # SciPy style, "incorrect" bindings with a hook
+ sources = [
+ util.getpath("tests", "src", "string", "gh25286_bc.pyf"),
+ util.getpath("tests", "src", "string", "gh25286.f90")
+ ]
+ module_name = "_char_handling_test"
+
+ def test_gh25286(self):
+ info = self.module.charint('T')
+ assert info == 2
diff --git a/numpy/f2py/tests/test_pyf_src.py b/numpy/f2py/tests/test_pyf_src.py
new file mode 100644
index 000000000000..f77ded2f31d4
--- /dev/null
+++ b/numpy/f2py/tests/test_pyf_src.py
@@ -0,0 +1,44 @@
+# This test is ported from numpy.distutils
+from numpy.f2py._src_pyf import process_str
+from numpy.testing import assert_equal
+
+
+pyf_src = """
+python module foo
+ <_rd=real,double precision>
+ interface
+ subroutine foosub(tol)
+ <_rd>, intent(in,out) :: tol
+ end subroutine foosub
+ end interface
+end python module foo
+"""
+
+expected_pyf = """
+python module foo
+ interface
+ subroutine sfoosub(tol)
+ real, intent(in,out) :: tol
+ end subroutine sfoosub
+ subroutine dfoosub(tol)
+ double precision, intent(in,out) :: tol
+ end subroutine dfoosub
+ end interface
+end python module foo
+"""
+
+
+def normalize_whitespace(s):
+ """
+ Remove leading and trailing whitespace, and convert internal
+ stretches of whitespace to a single space.
+ """
+ return ' '.join(s.split())
+
+
+def test_from_template():
+ """Regression test for gh-10712."""
+ pyf = process_str(pyf_src)
+ normalized_pyf = normalize_whitespace(pyf)
+ normalized_expected_pyf = normalize_whitespace(expected_pyf)
+ assert_equal(normalized_pyf, normalized_expected_pyf)