Skip to content

Commit

Permalink
Merge pull request #25287 from HaoZeke/gh25286
Browse files Browse the repository at this point in the history
BUG: Handle .pyf.src and fix SciPy [urgent]
  • Loading branch information
tylerjereddy committed Dec 1, 2023
2 parents b72c24e + 722b09c commit 44570a8
Show file tree
Hide file tree
Showing 9 changed files with 390 additions and 31 deletions.
9 changes: 8 additions & 1 deletion numpy/f2py/_backends/_meson.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import os
import errno
import shutil
import subprocess
Expand Down Expand Up @@ -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"""
Expand Down
239 changes: 239 additions & 0 deletions numpy/f2py/_src_pyf.py
Original file line number Diff line number Diff line change
@@ -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:
<p=d,s,z,c> where anywhere inside a block '<p>' 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:
<s,d,c,z>, a short form of the named, useful when no <p> 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 '\\<index>' then it will be replaced
by <index>-th expression.
Note that all '<..>' forms in a block must have the same number of
comma-separated entries.
Predefined named template rules:
<prefix=s,d,c,z>
<ftype=real,double precision,complex,double complex>
<ftypereal=real,double precision,\\0,\\1>
<ctype=float,double,complex_float,complex_double>
<ctypereal=float,double,\\0,\\1>
"""

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<index>\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<name>[\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>
<prefix=s,d,c,z>
<ftype=real,double precision,complex,double complex>
<ctype=float,double,complex_float,complex_double>
<ftypereal=real,double precision,\\0,\\1>
<ctypereal=float,double,\\0,\\1>
''')

# END OF CODE VENDORED FROM `numpy.distutils.from_template`
###########################################################
51 changes: 28 additions & 23 deletions numpy/f2py/cfuncs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 */
Expand Down
13 changes: 6 additions & 7 deletions numpy/f2py/f2py2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down
14 changes: 14 additions & 0 deletions numpy/f2py/tests/src/string/gh25286.f90
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions numpy/f2py/tests/src/string/gh25286.pyf
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 44570a8

Please sign in to comment.