Skip to content
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

BUG: Handle .pyf.src and fix SciPy [urgent] #25287

Merged
merged 8 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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