diff options
Diffstat (limited to 'numpy/f2py')
-rw-r--r-- | numpy/f2py/capi_maps.py | 8 | ||||
-rw-r--r-- | numpy/f2py/cfuncs.py | 9 | ||||
-rwxr-xr-x | numpy/f2py/crackfortran.py | 142 | ||||
-rw-r--r-- | numpy/f2py/diagnose.py | 12 | ||||
-rw-r--r-- | numpy/f2py/func2subr.py | 6 | ||||
-rw-r--r-- | numpy/f2py/symbolic.py | 4 | ||||
-rw-r--r-- | numpy/f2py/tests/src/crackfortran/gh23533.f | 5 | ||||
-rw-r--r-- | numpy/f2py/tests/src/crackfortran/gh23598.f90 | 4 | ||||
-rw-r--r-- | numpy/f2py/tests/src/crackfortran/gh23598Warn.f90 | 11 | ||||
-rw-r--r-- | numpy/f2py/tests/src/crackfortran/unicode_comment.f90 | 4 | ||||
-rw-r--r-- | numpy/f2py/tests/src/string/scalar_string.f90 | 9 | ||||
-rw-r--r-- | numpy/f2py/tests/test_character.py | 29 | ||||
-rw-r--r-- | numpy/f2py/tests/test_crackfortran.py | 78 | ||||
-rw-r--r-- | numpy/f2py/tests/test_f2py2e.py | 22 | ||||
-rw-r--r-- | numpy/f2py/tests/test_kind.py | 27 | ||||
-rw-r--r-- | numpy/f2py/tests/util.py | 20 |
16 files changed, 301 insertions, 89 deletions
diff --git a/numpy/f2py/capi_maps.py b/numpy/f2py/capi_maps.py index f07066a09..f0a7221b7 100644 --- a/numpy/f2py/capi_maps.py +++ b/numpy/f2py/capi_maps.py @@ -196,7 +196,7 @@ def load_f2cmap_file(f2cmap_file): # they use PARAMETERS in type specifications. try: outmess('Reading f2cmap from {!r} ...\n'.format(f2cmap_file)) - with open(f2cmap_file, 'r') as f: + with open(f2cmap_file) as f: d = eval(f.read().lower(), {}, {}) for k, d1 in d.items(): for k1 in d1.keys(): @@ -342,9 +342,9 @@ def getstrlength(var): def getarrdims(a, var, verbose=0): ret = {} if isstring(var) and not isarray(var): - ret['dims'] = getstrlength(var) - ret['size'] = ret['dims'] - ret['rank'] = '1' + ret['size'] = getstrlength(var) + ret['rank'] = '0' + ret['dims'] = '' elif isscalar(var): ret['size'] = '1' ret['rank'] = '0' diff --git a/numpy/f2py/cfuncs.py b/numpy/f2py/cfuncs.py index 741692562..2d27b6524 100644 --- a/numpy/f2py/cfuncs.py +++ b/numpy/f2py/cfuncs.py @@ -800,16 +800,23 @@ character_from_pyobj(character* v, PyObject *obj, const char *errmess) { } } { + /* TODO: This error (and most other) error handling needs cleaning. */ char mess[F2PY_MESSAGE_BUFFER_SIZE]; strcpy(mess, errmess); PyObject* err = PyErr_Occurred(); if (err == NULL) { err = PyExc_TypeError; + Py_INCREF(err); + } + else { + Py_INCREF(err); + PyErr_Clear(); } sprintf(mess + strlen(mess), " -- expected str|bytes|sequence-of-str-or-bytes, got "); f2py_describe(obj, mess + strlen(mess)); PyErr_SetString(err, mess); + Py_DECREF(err); } return 0; } @@ -1227,8 +1234,8 @@ static int try_pyarr_from_character(PyObject* obj, character* v) { strcpy(mess, "try_pyarr_from_character failed" " -- expected bytes array-scalar|array, got "); f2py_describe(obj, mess + strlen(mess)); + PyErr_SetString(err, mess); } - PyErr_SetString(err, mess); } return 0; } diff --git a/numpy/f2py/crackfortran.py b/numpy/f2py/crackfortran.py index 27e257c48..4871d2628 100755 --- a/numpy/f2py/crackfortran.py +++ b/numpy/f2py/crackfortran.py @@ -147,10 +147,11 @@ import os import copy import platform import codecs +from pathlib import Path try: - import chardet + import charset_normalizer except ImportError: - chardet = None + charset_normalizer = None from . import __version__ @@ -289,69 +290,69 @@ def undo_rmbadname(names): return [undo_rmbadname1(_m) for _m in names] -def getextension(name): - i = name.rfind('.') - if i == -1: - return '' - if '\\' in name[i:]: - return '' - if '/' in name[i:]: - return '' - return name[i + 1:] - -is_f_file = re.compile(r'.*\.(for|ftn|f77|f)\Z', re.I).match _has_f_header = re.compile(r'-\*-\s*fortran\s*-\*-', re.I).search _has_f90_header = re.compile(r'-\*-\s*f90\s*-\*-', re.I).search _has_fix_header = re.compile(r'-\*-\s*fix\s*-\*-', re.I).search _free_f90_start = re.compile(r'[^c*]\s*[^\s\d\t]', re.I).match +# Extensions +COMMON_FREE_EXTENSIONS = ['.f90', '.f95', '.f03', '.f08'] +COMMON_FIXED_EXTENSIONS = ['.for', '.ftn', '.f77', '.f'] + def openhook(filename, mode): """Ensures that filename is opened with correct encoding parameter. - This function uses chardet package, when available, for - determining the encoding of the file to be opened. When chardet is - not available, the function detects only UTF encodings, otherwise, - ASCII encoding is used as fallback. + This function uses charset_normalizer package, when available, for + determining the encoding of the file to be opened. When charset_normalizer + is not available, the function detects only UTF encodings, otherwise, ASCII + encoding is used as fallback. """ - bytes = min(32, os.path.getsize(filename)) - with open(filename, 'rb') as f: - raw = f.read(bytes) - if raw.startswith(codecs.BOM_UTF8): - encoding = 'UTF-8-SIG' - elif raw.startswith((codecs.BOM_UTF32_LE, codecs.BOM_UTF32_BE)): - encoding = 'UTF-32' - elif raw.startswith((codecs.BOM_LE, codecs.BOM_BE)): - encoding = 'UTF-16' + # Reads in the entire file. Robust detection of encoding. + # Correctly handles comments or late stage unicode characters + # gh-22871 + if charset_normalizer is not None: + encoding = charset_normalizer.from_path(filename).best().encoding else: - if chardet is not None: - encoding = chardet.detect(raw)['encoding'] - else: - # hint: install chardet to ensure correct encoding handling - encoding = 'ascii' + # hint: install charset_normalizer for correct encoding handling + # No need to read the whole file for trying with startswith + nbytes = min(32, os.path.getsize(filename)) + with open(filename, 'rb') as fhandle: + raw = fhandle.read(nbytes) + if raw.startswith(codecs.BOM_UTF8): + encoding = 'UTF-8-SIG' + elif raw.startswith((codecs.BOM_UTF32_LE, codecs.BOM_UTF32_BE)): + encoding = 'UTF-32' + elif raw.startswith((codecs.BOM_LE, codecs.BOM_BE)): + encoding = 'UTF-16' + else: + # Fallback, without charset_normalizer + encoding = 'ascii' return open(filename, mode, encoding=encoding) -def is_free_format(file): +def is_free_format(fname): """Check if file is in free format Fortran.""" # f90 allows both fixed and free format, assuming fixed unless # signs of free format are detected. - result = 0 - with openhook(file, 'r') as f: - line = f.readline() + result = False + if Path(fname).suffix.lower() in COMMON_FREE_EXTENSIONS: + result = True + with openhook(fname, 'r') as fhandle: + line = fhandle.readline() n = 15 # the number of non-comment lines to scan for hints if _has_f_header(line): n = 0 elif _has_f90_header(line): n = 0 - result = 1 + result = True while n > 0 and line: if line[0] != '!' and line.strip(): n -= 1 if (line[0] != '\t' and _free_f90_start(line[:5])) or line[-2:-1] == '&': - result = 1 + result = True break - line = f.readline() + line = fhandle.readline() return result @@ -394,7 +395,7 @@ def readfortrancode(ffile, dowithline=show, istop=1): except UnicodeDecodeError as msg: raise Exception( f'readfortrancode: reading {fin.filename()}#{fin.lineno()}' - f' failed with\n{msg}.\nIt is likely that installing chardet' + f' failed with\n{msg}.\nIt is likely that installing charset_normalizer' ' package will help f2py determine the input file encoding' ' correctly.') if not l: @@ -407,7 +408,7 @@ def readfortrancode(ffile, dowithline=show, istop=1): strictf77 = 0 sourcecodeform = 'fix' ext = os.path.splitext(currentfilename)[1] - if is_f_file(currentfilename) and \ + if Path(currentfilename).suffix.lower() in COMMON_FIXED_EXTENSIONS and \ not (_has_f90_header(l) or _has_fix_header(l)): strictf77 = 1 elif is_free_format(currentfilename) and not _has_fix_header(l): @@ -612,15 +613,15 @@ beginpattern90 = re.compile( groupends = (r'end|endprogram|endblockdata|endmodule|endpythonmodule|' r'endinterface|endsubroutine|endfunction') endpattern = re.compile( - beforethisafter % ('', groupends, groupends, r'.*'), re.I), 'end' + beforethisafter % ('', groupends, groupends, '.*'), re.I), 'end' endifs = r'end\s*(if|do|where|select|while|forall|associate|block|' + \ r'critical|enum|team)' endifpattern = re.compile( - beforethisafter % (r'[\w]*?', endifs, endifs, r'[\w\s]*'), re.I), 'endif' + beforethisafter % (r'[\w]*?', endifs, endifs, '.*'), re.I), 'endif' # moduleprocedures = r'module\s*procedure' moduleprocedurepattern = re.compile( - beforethisafter % ('', moduleprocedures, moduleprocedures, r'.*'), re.I), \ + beforethisafter % ('', moduleprocedures, moduleprocedures, '.*'), re.I), \ 'moduleprocedure' implicitpattern = re.compile( beforethisafter % ('', 'implicit', 'implicit', '.*'), re.I), 'implicit' @@ -934,7 +935,7 @@ typedefpattern = re.compile( r'(?:,(?P<attributes>[\w(),]+))?(::)?(?P<name>\b[a-z$_][\w$]*\b)' r'(?:\((?P<params>[\w,]*)\))?\Z', re.I) nameargspattern = re.compile( - r'\s*(?P<name>\b[\w$]+\b)\s*(@\(@\s*(?P<args>[\w\s,]*)\s*@\)@|)\s*((result(\s*@\(@\s*(?P<result>\b[\w$]+\b)\s*@\)@|))|(bind\s*@\(@\s*(?P<bind>.*)\s*@\)@))*\s*\Z', re.I) + r'\s*(?P<name>\b[\w$]+\b)\s*(@\(@\s*(?P<args>[\w\s,]*)\s*@\)@|)\s*((result(\s*@\(@\s*(?P<result>\b[\w$]+\b)\s*@\)@|))|(bind\s*@\(@\s*(?P<bind>(?:(?!@\)@).)*)\s*@\)@))*\s*\Z', re.I) operatorpattern = re.compile( r'\s*(?P<scheme>(operator|assignment))' r'@\(@\s*(?P<name>[^)]+)\s*@\)@\s*\Z', re.I) @@ -1739,6 +1740,28 @@ def updatevars(typespec, selector, attrspec, entitydecl): d1[k] = unmarkouterparen(d1[k]) else: del d1[k] + + if 'len' in d1: + if typespec in ['complex', 'integer', 'logical', 'real']: + if ('kindselector' not in edecl) or (not edecl['kindselector']): + edecl['kindselector'] = {} + edecl['kindselector']['*'] = d1['len'] + del d1['len'] + elif typespec == 'character': + if ('charselector' not in edecl) or (not edecl['charselector']): + edecl['charselector'] = {} + if 'len' in edecl['charselector']: + del edecl['charselector']['len'] + edecl['charselector']['*'] = d1['len'] + del d1['len'] + + if 'init' in d1: + if '=' in edecl and (not edecl['='] == d1['init']): + outmess('updatevars: attempt to change the init expression of "%s" ("%s") to "%s". Ignoring.\n' % ( + ename, edecl['='], d1['init'])) + else: + edecl['='] = d1['init'] + if 'len' in d1 and 'array' in d1: if d1['len'] == '': d1['len'] = d1['array'] @@ -1748,6 +1771,7 @@ def updatevars(typespec, selector, attrspec, entitydecl): del d1['len'] errmess('updatevars: "%s %s" is mapped to "%s %s(%s)"\n' % ( typespec, e, typespec, ename, d1['array'])) + if 'array' in d1: dm = 'dimension(%s)' % d1['array'] if 'attrspec' not in edecl or (not edecl['attrspec']): @@ -1761,23 +1785,6 @@ def updatevars(typespec, selector, attrspec, entitydecl): % (ename, dm1, dm)) break - if 'len' in d1: - if typespec in ['complex', 'integer', 'logical', 'real']: - if ('kindselector' not in edecl) or (not edecl['kindselector']): - edecl['kindselector'] = {} - edecl['kindselector']['*'] = d1['len'] - elif typespec == 'character': - if ('charselector' not in edecl) or (not edecl['charselector']): - edecl['charselector'] = {} - if 'len' in edecl['charselector']: - del edecl['charselector']['len'] - edecl['charselector']['*'] = d1['len'] - if 'init' in d1: - if '=' in edecl and (not edecl['='] == d1['init']): - outmess('updatevars: attempt to change the init expression of "%s" ("%s") to "%s". Ignoring.\n' % ( - ename, edecl['='], d1['init'])) - else: - edecl['='] = d1['init'] else: outmess('updatevars: could not crack entity declaration "%s". Ignoring.\n' % ( ename + m.group('after'))) @@ -2386,19 +2393,19 @@ def _selected_int_kind_func(r): def _selected_real_kind_func(p, r=0, radix=0): # XXX: This should be processor dependent - # This is only good for 0 <= p <= 20 + # This is only verified for 0 <= p <= 20, possibly good for p <= 33 and above if p < 7: return 4 if p < 16: return 8 machine = platform.machine().lower() - if machine.startswith(('aarch64', 'power', 'ppc', 'riscv', 's390x', 'sparc')): - if p <= 20: + if machine.startswith(('aarch64', 'arm64', 'power', 'ppc', 'riscv', 's390x', 'sparc')): + if p <= 33: return 16 else: if p < 19: return 10 - elif p <= 20: + elif p <= 33: return 16 return -1 @@ -2849,6 +2856,11 @@ def analyzevars(block): kindselect, charselect, typename = cracktypespec( typespec, selector) vars[n]['typespec'] = typespec + try: + if block['result']: + vars[block['result']]['typespec'] = typespec + except Exception: + pass if kindselect: if 'kind' in kindselect: try: diff --git a/numpy/f2py/diagnose.py b/numpy/f2py/diagnose.py index 21ee399f0..86d7004ab 100644 --- a/numpy/f2py/diagnose.py +++ b/numpy/f2py/diagnose.py @@ -30,15 +30,15 @@ def run(): try: import numpy has_newnumpy = 1 - except ImportError: - print('Failed to import new numpy:', sys.exc_info()[1]) + except ImportError as e: + print('Failed to import new numpy:', e) has_newnumpy = 0 try: from numpy.f2py import f2py2e has_f2py2e = 1 - except ImportError: - print('Failed to import f2py2e:', sys.exc_info()[1]) + except ImportError as e: + print('Failed to import f2py2e:', e) has_f2py2e = 0 try: @@ -48,8 +48,8 @@ def run(): try: import numpy_distutils has_numpy_distutils = 1 - except ImportError: - print('Failed to import numpy_distutils:', sys.exc_info()[1]) + except ImportError as e: + print('Failed to import numpy_distutils:', e) has_numpy_distutils = 0 if has_newnumpy: diff --git a/numpy/f2py/func2subr.py b/numpy/f2py/func2subr.py index 2a05f065b..cc3cdc5b4 100644 --- a/numpy/f2py/func2subr.py +++ b/numpy/f2py/func2subr.py @@ -119,6 +119,12 @@ def createfuncwrapper(rout, signature=0): sargs = ', '.join(args) if f90mode: + # gh-23598 fix warning + # Essentially, this gets called again with modules where the name of the + # function is added to the arguments, which is not required, and removed + sargs = sargs.replace(f"{name}, ", '') + args = [arg for arg in args if arg != name] + rout['args'] = args add('subroutine f2pywrap_%s_%s (%s)' % (rout['modulename'], name, sargs)) if not signature: diff --git a/numpy/f2py/symbolic.py b/numpy/f2py/symbolic.py index c2ab0f140..b1b9f5b6a 100644 --- a/numpy/f2py/symbolic.py +++ b/numpy/f2py/symbolic.py @@ -801,7 +801,7 @@ def normalize(obj): else: _pairs_add(d, t, c) if len(d) == 0: - # TODO: deterimine correct kind + # TODO: determine correct kind return as_number(0) elif len(d) == 1: (t, c), = d.items() @@ -836,7 +836,7 @@ def normalize(obj): else: _pairs_add(d, b, e) if len(d) == 0 or coeff == 0: - # TODO: deterimine correct kind + # TODO: determine correct kind assert isinstance(coeff, number_types) return as_number(coeff) elif len(d) == 1: diff --git a/numpy/f2py/tests/src/crackfortran/gh23533.f b/numpy/f2py/tests/src/crackfortran/gh23533.f new file mode 100644 index 000000000..db522afa7 --- /dev/null +++ b/numpy/f2py/tests/src/crackfortran/gh23533.f @@ -0,0 +1,5 @@ + SUBROUTINE EXAMPLE( ) + IF( .TRUE. ) THEN + CALL DO_SOMETHING() + END IF ! ** .TRUE. ** + END diff --git a/numpy/f2py/tests/src/crackfortran/gh23598.f90 b/numpy/f2py/tests/src/crackfortran/gh23598.f90 new file mode 100644 index 000000000..e0dffb5ef --- /dev/null +++ b/numpy/f2py/tests/src/crackfortran/gh23598.f90 @@ -0,0 +1,4 @@ +integer function intproduct(a, b) result(res) + integer, intent(in) :: a, b + res = a*b +end function diff --git a/numpy/f2py/tests/src/crackfortran/gh23598Warn.f90 b/numpy/f2py/tests/src/crackfortran/gh23598Warn.f90 new file mode 100644 index 000000000..3b44efc5e --- /dev/null +++ b/numpy/f2py/tests/src/crackfortran/gh23598Warn.f90 @@ -0,0 +1,11 @@ +module test_bug + implicit none + private + public :: intproduct + +contains + integer function intproduct(a, b) result(res) + integer, intent(in) :: a, b + res = a*b + end function +end module diff --git a/numpy/f2py/tests/src/crackfortran/unicode_comment.f90 b/numpy/f2py/tests/src/crackfortran/unicode_comment.f90 new file mode 100644 index 000000000..13515ce98 --- /dev/null +++ b/numpy/f2py/tests/src/crackfortran/unicode_comment.f90 @@ -0,0 +1,4 @@ +subroutine foo(x) + real(8), intent(in) :: x + ! Écrit à l'écran la valeur de x +end subroutine diff --git a/numpy/f2py/tests/src/string/scalar_string.f90 b/numpy/f2py/tests/src/string/scalar_string.f90 new file mode 100644 index 000000000..f8f076172 --- /dev/null +++ b/numpy/f2py/tests/src/string/scalar_string.f90 @@ -0,0 +1,9 @@ +MODULE string_test + + character(len=8) :: string + character string77 * 8 + + character(len=12), dimension(5,7) :: strarr + character strarr77(5,7) * 12 + +END MODULE string_test diff --git a/numpy/f2py/tests/test_character.py b/numpy/f2py/tests/test_character.py index b54b4d981..0bb0f4290 100644 --- a/numpy/f2py/tests/test_character.py +++ b/numpy/f2py/tests/test_character.py @@ -457,9 +457,10 @@ class TestMiscCharacter(util.F2PyTest): character(len=*), intent(in) :: x(:) !f2py intent(out) x integer :: i - do i=1, size(x) - print*, "x(",i,")=", x(i) - end do + ! Uncomment for debug printing: + !do i=1, size(x) + ! print*, "x(",i,")=", x(i) + !end do end subroutine {fprefix}_gh4519 pure function {fprefix}_gh3425(x) result (y) @@ -568,3 +569,25 @@ class TestMiscCharacter(util.F2PyTest): assert_equal(len(a), 2) assert_raises(Exception, lambda: f(b'c')) + + +class TestStringScalarArr(util.F2PyTest): + sources = [util.getpath("tests", "src", "string", "scalar_string.f90")] + + @pytest.mark.slow + def test_char(self): + for out in (self.module.string_test.string, + self.module.string_test.string77): + expected = () + assert out.shape == expected + expected = '|S8' + assert out.dtype == expected + + @pytest.mark.slow + def test_char_arr(self): + for out in (self.module.string_test.strarr, + self.module.string_test.strarr77): + expected = (5,7) + assert out.shape == expected + expected = '|S12' + assert out.dtype == expected diff --git a/numpy/f2py/tests/test_crackfortran.py b/numpy/f2py/tests/test_crackfortran.py index dcf8760db..49bfc13af 100644 --- a/numpy/f2py/tests/test_crackfortran.py +++ b/numpy/f2py/tests/test_crackfortran.py @@ -1,7 +1,10 @@ +import importlib import codecs +import time +import unicodedata import pytest import numpy as np -from numpy.f2py.crackfortran import markinnerspaces +from numpy.f2py.crackfortran import markinnerspaces, nameargspattern from . import util from numpy.f2py import crackfortran import textwrap @@ -132,6 +135,7 @@ class TestMarkinnerspaces: assert markinnerspaces("a 'b c' 'd e'") == "a 'b@_@c' 'd@_@e'" assert markinnerspaces(r'a "b c" "d e"') == r'a "b@_@c" "d@_@e"' + class TestDimSpec(util.F2PyTest): """This test suite tests various expressions that are used as dimension specifications. @@ -241,6 +245,7 @@ class TestModuleDeclaration: assert len(mod) == 1 assert mod[0]["vars"]["abar"]["="] == "bar('abar')" + class TestEval(util.F2PyTest): def test_eval_scalar(self): eval_scalar = crackfortran._eval_scalar @@ -257,13 +262,76 @@ class TestFortranReader(util.F2PyTest): def test_input_encoding(self, tmp_path, encoding): # gh-635 f_path = tmp_path / f"input_with_{encoding}_encoding.f90" - # explicit BOM is required for UTF8 - bom = {'utf-8': codecs.BOM_UTF8}.get(encoding, b'') with f_path.open('w', encoding=encoding) as ff: - ff.write(bom.decode(encoding) + - """ + ff.write(""" subroutine foo() end subroutine foo """) mod = crackfortran.crackfortran([str(f_path)]) assert mod[0]['name'] == 'foo' + + +class TestUnicodeComment(util.F2PyTest): + sources = [util.getpath("tests", "src", "crackfortran", "unicode_comment.f90")] + + @pytest.mark.skipif( + (importlib.util.find_spec("charset_normalizer") is None), + reason="test requires charset_normalizer which is not installed", + ) + def test_encoding_comment(self): + self.module.foo(3) + + +class TestNameArgsPatternBacktracking: + @pytest.mark.parametrize( + ['adversary'], + [ + ('@)@bind@(@',), + ('@)@bind @(@',), + ('@)@bind foo bar baz@(@',) + ] + ) + def test_nameargspattern_backtracking(self, adversary): + '''address ReDOS vulnerability: + https://github.com/numpy/numpy/issues/23338''' + trials_per_batch = 12 + batches_per_regex = 4 + start_reps, end_reps = 15, 25 + for ii in range(start_reps, end_reps): + repeated_adversary = adversary * ii + # test times in small batches. + # this gives us more chances to catch a bad regex + # while still catching it before too long if it is bad + for _ in range(batches_per_regex): + times = [] + for _ in range(trials_per_batch): + t0 = time.perf_counter() + mtch = nameargspattern.search(repeated_adversary) + times.append(time.perf_counter() - t0) + # our pattern should be much faster than 0.2s per search + # it's unlikely that a bad regex will pass even on fast CPUs + assert np.median(times) < 0.2 + assert not mtch + # if the adversary is capped with @)@, it becomes acceptable + # according to the old version of the regex. + # that should still be true. + good_version_of_adversary = repeated_adversary + '@)@' + assert nameargspattern.search(good_version_of_adversary) + + +class TestFunctionReturn(util.F2PyTest): + sources = [util.getpath("tests", "src", "crackfortran", "gh23598.f90")] + + def test_function_rettype(self): + # gh-23598 + assert self.module.intproduct(3, 4) == 12 + + +class TestFortranGroupCounters(util.F2PyTest): + def test_end_if_comment(self): + # gh-23533 + fpath = util.getpath("tests", "src", "crackfortran", "gh23533.f") + try: + crackfortran.crackfortran([str(fpath)]) + except Exception as exc: + assert False, f"'crackfortran.crackfortran' raised an exception {exc}" diff --git a/numpy/f2py/tests/test_f2py2e.py b/numpy/f2py/tests/test_f2py2e.py index 2c10f046f..5f7b56a68 100644 --- a/numpy/f2py/tests/test_f2py2e.py +++ b/numpy/f2py/tests/test_f2py2e.py @@ -63,6 +63,15 @@ def hello_world_f90(tmpdir_factory): @pytest.fixture(scope="session") +def gh23598_warn(tmpdir_factory): + """F90 file for testing warnings in gh23598""" + fdat = util.getpath("tests", "src", "crackfortran", "gh23598Warn.f90").read_text() + fn = tmpdir_factory.getbasetemp() / "gh23598Warn.f90" + 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""" fdat = util.getpath("tests", "src", "cli", "hi77.f").read_text() @@ -91,6 +100,19 @@ def f2cmap_f90(tmpdir_factory): return fn +def test_gh23598_warn(capfd, gh23598_warn, monkeypatch): + foutl = get_io_paths(gh23598_warn, mname="test") + ipath = foutl.f90inp + monkeypatch.setattr( + sys, "argv", + f'f2py {ipath} -m test'.split()) + + with util.switchdir(ipath.parent): + f2pycli() # Generate files + wrapper = foutl.wrap90.read_text() + assert "intproductf2pywrap, intpr" not in wrapper + + def test_gen_pyf(capfd, hello_world_f90, monkeypatch): """Ensures that a signature file is generated via the CLI CLI :: -h diff --git a/numpy/f2py/tests/test_kind.py b/numpy/f2py/tests/test_kind.py index f0cb61fb6..69b85aaad 100644 --- a/numpy/f2py/tests/test_kind.py +++ b/numpy/f2py/tests/test_kind.py @@ -1,5 +1,6 @@ import os import pytest +import platform from numpy.f2py.crackfortran import ( _selected_int_kind_func as selected_int_kind, @@ -11,8 +12,8 @@ from . import util class TestKind(util.F2PyTest): sources = [util.getpath("tests", "src", "kind", "foo.f90")] - def test_all(self): - selectedrealkind = self.module.selectedrealkind + def test_int(self): + """Test `int` kind_func for integers up to 10**40.""" selectedintkind = self.module.selectedintkind for i in range(40): @@ -20,7 +21,27 @@ class TestKind(util.F2PyTest): i ), f"selectedintkind({i}): expected {selected_int_kind(i)!r} but got {selectedintkind(i)!r}" - for i in range(20): + def test_real(self): + """ + Test (processor-dependent) `real` kind_func for real numbers + of up to 31 digits precision (extended/quadruple). + """ + selectedrealkind = self.module.selectedrealkind + + for i in range(32): + assert selectedrealkind(i) == selected_real_kind( + i + ), f"selectedrealkind({i}): expected {selected_real_kind(i)!r} but got {selectedrealkind(i)!r}" + + @pytest.mark.xfail(platform.machine().lower().startswith("ppc"), + reason="Some PowerPC may not support full IEEE 754 precision") + def test_quad_precision(self): + """ + Test kind_func for quadruple precision [`real(16)`] of 32+ digits . + """ + selectedrealkind = self.module.selectedrealkind + + for i in range(32, 40): assert selectedrealkind(i) == selected_real_kind( i ), f"selectedrealkind({i}): expected {selected_real_kind(i)!r} but got {selectedrealkind(i)!r}" diff --git a/numpy/f2py/tests/util.py b/numpy/f2py/tests/util.py index 1534c4e7d..26fa7e49d 100644 --- a/numpy/f2py/tests/util.py +++ b/numpy/f2py/tests/util.py @@ -6,6 +6,7 @@ Utility functions for - determining paths to tests """ +import glob import os import sys import subprocess @@ -30,6 +31,10 @@ from importlib import import_module _module_dir = None _module_num = 5403 +if sys.platform == "cygwin": + NUMPY_INSTALL_ROOT = Path(__file__).parent.parent.parent + _module_list = list(NUMPY_INSTALL_ROOT.glob("**/*.dll")) + def _cleanup(): global _module_dir @@ -147,6 +152,21 @@ def build_module(source_files, options=[], skip=[], only=[], module_name=None): for fn in dst_sources: os.unlink(fn) + # Rebase (Cygwin-only) + if sys.platform == "cygwin": + # If someone starts deleting modules after import, this will + # need to change to record how big each module is, rather than + # relying on rebase being able to find that from the files. + _module_list.extend( + glob.glob(os.path.join(d, "{:s}*".format(module_name))) + ) + subprocess.check_call( + ["/usr/bin/rebase", "--database", "--oblivious", "--verbose"] + + _module_list + ) + + + # Import return import_module(module_name) |