From 9240ebacb7fab1cbafbe94c2d60d26d0cde46bca Mon Sep 17 00:00:00 2001 From: Rohit Goswami Date: Thu, 30 Nov 2023 23:17:46 +0000 Subject: [PATCH 1/8] BUG: Handle .pyf.src again Closes gh-25286 --- numpy/f2py/f2py2e.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/numpy/f2py/f2py2e.py b/numpy/f2py/f2py2e.py index 07383a2ba4ce..97ee1aacf915 100755 --- a/numpy/f2py/f2py2e.py +++ b/numpy/f2py/f2py2e.py @@ -455,17 +455,16 @@ 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 = ( - args.module_name - ) # Directly use from args when -h is present - else: - 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 + modname = None + if "-h" not in comline_list: + modname = validate_modulename(pyf_files, args.module_name) + # If -h is present or no valid modname found, use the module name from args + if modname is None and args.module_name: + modname = args.module_name + # If no module name is specified in args or found in pyf_files, use default + if modname is None: + modname = "untitled" + comline_list += ["-m", modname] # needed for the rest of scaninputline # gh-22819 -- end files, options = scaninputline(comline_list) auxfuncs.options = options From 6aceaa9b5535f4a69ec946c118ebf4ffee28806c Mon Sep 17 00:00:00 2001 From: Rohit Goswami Date: Fri, 1 Dec 2023 07:37:11 +0000 Subject: [PATCH 2/8] MAINT: Migrate pyf.src from distutils [skip-ci] --- numpy/f2py/_src_pyf.py | 239 +++++++++++++++++++++++++++++++ numpy/f2py/tests/test_pyf_src.py | 44 ++++++ 2 files changed, 283 insertions(+) create mode 100644 numpy/f2py/_src_pyf.py create mode 100644 numpy/f2py/tests/test_pyf_src.py 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/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) From 3d2ec8f626c0696c9d6ac5a6456d9ec6dc0fa4e1 Mon Sep 17 00:00:00 2001 From: Rohit Goswami Date: Fri, 1 Dec 2023 15:20:30 +0000 Subject: [PATCH 3/8] Revert "BUG: Handle .pyf.src again" This reverts commit 9240ebacb7fab1cbafbe94c2d60d26d0cde46bca. --- numpy/f2py/f2py2e.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/numpy/f2py/f2py2e.py b/numpy/f2py/f2py2e.py index 97ee1aacf915..07383a2ba4ce 100755 --- a/numpy/f2py/f2py2e.py +++ b/numpy/f2py/f2py2e.py @@ -455,16 +455,17 @@ 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 = None - if "-h" not in comline_list: - modname = validate_modulename(pyf_files, args.module_name) - # If -h is present or no valid modname found, use the module name from args - if modname is None and args.module_name: - modname = args.module_name - # If no module name is specified in args or found in pyf_files, use default - if modname is None: - modname = "untitled" - comline_list += ["-m", modname] # needed for the rest of scaninputline + modname = "untitled" # Default + if args.module_name: + if "-h" in comline_list: + modname = ( + args.module_name + ) # Directly use from args when -h is present + else: + 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 # gh-22819 -- end files, options = scaninputline(comline_list) auxfuncs.options = options From 3bf06f0866365039e6cd214c5a3670efc6d9b0c0 Mon Sep 17 00:00:00 2001 From: Rohit Goswami Date: Fri, 1 Dec 2023 16:23:57 +0000 Subject: [PATCH 4/8] BUG: Ensure defines are on newlines [f2py] --- numpy/f2py/cfuncs.py | 51 ++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 23 deletions(-) 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 */ From 8d47e18dba4ba6af703d6bcb84846d8519daafe8 Mon Sep 17 00:00:00 2001 From: Rohit Goswami Date: Fri, 1 Dec 2023 21:48:12 +0000 Subject: [PATCH 5/8] BUG: Don't lower .pyf files --- numpy/f2py/f2py2e.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/numpy/f2py/f2py2e.py b/numpy/f2py/f2py2e.py index 07383a2ba4ce..fb7cd04aebef 100755 --- a/numpy/f2py/f2py2e.py +++ b/numpy/f2py/f2py2e.py @@ -688,12 +688,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( From cf70d72899252e415770c13a61746cf1ba3703bf Mon Sep 17 00:00:00 2001 From: Rohit Goswami Date: Fri, 1 Dec 2023 22:25:18 +0000 Subject: [PATCH 6/8] TST: Add a test mimicing some flapack checks --- numpy/f2py/tests/src/string/gh25286.f90 | 14 +++++++++++ numpy/f2py/tests/src/string/gh25286.pyf | 12 ++++++++++ numpy/f2py/tests/src/string/gh25286_bc.pyf | 12 ++++++++++ numpy/f2py/tests/test_character.py | 27 ++++++++++++++++++++++ 4 files changed, 65 insertions(+) create mode 100644 numpy/f2py/tests/src/string/gh25286.f90 create mode 100644 numpy/f2py/tests/src/string/gh25286.pyf create mode 100644 numpy/f2py/tests/src/string/gh25286_bc.pyf 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 From 7e7e457c429fbb80f0b430d3510906d8b24fca05 Mon Sep 17 00:00:00 2001 From: Rohit Goswami Date: Fri, 1 Dec 2023 22:49:38 +0000 Subject: [PATCH 7/8] BUG: Don't autogenerate untitled modules [f2py] --- numpy/f2py/f2py2e.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/numpy/f2py/f2py2e.py b/numpy/f2py/f2py2e.py index fb7cd04aebef..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 From 722b09c024419dbeffcd25cb0be2604d1e09afe1 Mon Sep 17 00:00:00 2001 From: Rohit Goswami Date: Fri, 1 Dec 2023 22:55:14 +0000 Subject: [PATCH 8/8] MAINT: Overwrite compiled extensions with meson Address https://github.com/numpy/numpy/issues/24874#issuecomment-1835632293 --- numpy/f2py/_backends/_meson.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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"""