diff options
Diffstat (limited to 'numpy')
-rw-r--r-- | numpy/f2py/auxfuncs.py | 71 | ||||
-rw-r--r-- | numpy/f2py/capi_maps.py | 61 | ||||
-rw-r--r-- | numpy/f2py/cb_rules.py | 25 | ||||
-rw-r--r-- | numpy/f2py/cfuncs.py | 133 | ||||
-rw-r--r-- | numpy/f2py/common_rules.py | 8 | ||||
-rwxr-xr-x | numpy/f2py/crackfortran.py | 243 | ||||
-rw-r--r-- | numpy/f2py/f90mod_rules.py | 20 | ||||
-rw-r--r-- | numpy/f2py/func2subr.py | 27 | ||||
-rwxr-xr-x | numpy/f2py/rules.py | 183 | ||||
-rw-r--r-- | numpy/f2py/src/fortranobject.c | 461 | ||||
-rw-r--r-- | numpy/f2py/src/fortranobject.h | 28 | ||||
-rw-r--r-- | numpy/f2py/tests/src/array_from_pyobj/wrapmodule.c | 7 | ||||
-rw-r--r-- | numpy/f2py/tests/test_array_from_pyobj.py | 81 | ||||
-rw-r--r-- | numpy/f2py/tests/test_callback.py | 22 | ||||
-rw-r--r-- | numpy/f2py/tests/test_character.py | 570 | ||||
-rw-r--r-- | numpy/f2py/tests/test_crackfortran.py | 31 | ||||
-rw-r--r-- | numpy/f2py/tests/test_docs.py | 55 | ||||
-rw-r--r-- | numpy/f2py/tests/test_return_character.py | 2 | ||||
-rw-r--r-- | numpy/f2py/tests/util.py | 10 |
19 files changed, 1693 insertions, 345 deletions
diff --git a/numpy/f2py/auxfuncs.py b/numpy/f2py/auxfuncs.py index c8f2067c9..98c9eff78 100644 --- a/numpy/f2py/auxfuncs.py +++ b/numpy/f2py/auxfuncs.py @@ -28,10 +28,12 @@ __all__ = [ 'getfortranname', 'getpymethoddef', 'getrestdoc', 'getusercode', 'getusercode1', 'hasbody', 'hascallstatement', 'hascommon', 'hasexternals', 'hasinitvalue', 'hasnote', 'hasresultnote', - 'isallocatable', 'isarray', 'isarrayofstrings', 'iscomplex', + 'isallocatable', 'isarray', 'isarrayofstrings', + 'ischaracter', 'ischaracterarray', 'ischaracter_or_characterarray', + 'iscomplex', 'iscomplexarray', 'iscomplexfunction', 'iscomplexfunction_warn', 'isdouble', 'isdummyroutine', 'isexternal', 'isfunction', - 'isfunction_wrap', 'isint1array', 'isinteger', 'isintent_aux', + 'isfunction_wrap', 'isint1', 'isint1array', 'isinteger', 'isintent_aux', 'isintent_c', 'isintent_callback', 'isintent_copy', 'isintent_dict', 'isintent_hide', 'isintent_in', 'isintent_inout', 'isintent_inplace', 'isintent_nothide', 'isintent_out', 'isintent_overwrite', 'islogical', @@ -39,7 +41,8 @@ __all__ = [ 'islong_doublefunction', 'islong_long', 'islong_longfunction', 'ismodule', 'ismoduleroutine', 'isoptional', 'isprivate', 'isrequired', 'isroutine', 'isscalar', 'issigned_long_longarray', 'isstring', - 'isstringarray', 'isstringfunction', 'issubroutine', + 'isstringarray', 'isstring_or_stringarray', 'isstringfunction', + 'issubroutine', 'issubroutine_wrap', 'isthreadsafe', 'isunsigned', 'isunsigned_char', 'isunsigned_chararray', 'isunsigned_long_long', 'isunsigned_long_longarray', 'isunsigned_short', @@ -68,24 +71,41 @@ def debugcapi(var): return 'capi' in debugoptions +def _ischaracter(var): + return 'typespec' in var and var['typespec'] == 'character' and \ + not isexternal(var) + + def _isstring(var): return 'typespec' in var and var['typespec'] == 'character' and \ not isexternal(var) -def isstring(var): - return _isstring(var) and not isarray(var) +def ischaracter_or_characterarray(var): + return _ischaracter(var) and 'charselector' not in var def ischaracter(var): - return isstring(var) and 'charselector' not in var + return ischaracter_or_characterarray(var) and not isarray(var) + + +def ischaracterarray(var): + return ischaracter_or_characterarray(var) and isarray(var) + + +def isstring_or_stringarray(var): + return _ischaracter(var) and 'charselector' in var + + +def isstring(var): + return isstring_or_stringarray(var) and not isarray(var) def isstringarray(var): - return isarray(var) and _isstring(var) + return isstring_or_stringarray(var) and isarray(var) -def isarrayofstrings(var): +def isarrayofstrings(var): # obsolete? # leaving out '*' for now so that `character*(*) a(m)` and `character # a(m,*)` are treated differently. Luckily `character**` is illegal. return isstringarray(var) and var['dimension'][-1] == '(*)' @@ -126,6 +146,11 @@ def get_kind(var): pass +def isint1(var): + return var.get('typespec') == 'integer' \ + and get_kind(var) == '1' and not isarray(var) + + def islong_long(var): if not isscalar(var): return 0 @@ -426,6 +451,7 @@ def isintent_hide(var): ('out' in var['intent'] and 'in' not in var['intent'] and (not l_or(isintent_inout, isintent_inplace)(var))))) + def isintent_nothide(var): return not isintent_hide(var) @@ -469,6 +495,7 @@ def isintent_aligned8(var): def isintent_aligned16(var): return 'aligned16' in var.get('intent', []) + isintent_dict = {isintent_in: 'INTENT_IN', isintent_inout: 'INTENT_INOUT', isintent_out: 'INTENT_OUT', isintent_hide: 'INTENT_HIDE', isintent_cache: 'INTENT_CACHE', @@ -566,19 +593,19 @@ class throw_error: def l_and(*f): - l, l2 = 'lambda v', [] + l1, l2 = 'lambda v', [] for i in range(len(f)): - l = '%s,f%d=f[%d]' % (l, i, i) + l1 = '%s,f%d=f[%d]' % (l1, i, i) l2.append('f%d(v)' % (i)) - return eval('%s:%s' % (l, ' and '.join(l2))) + return eval('%s:%s' % (l1, ' and '.join(l2))) def l_or(*f): - l, l2 = 'lambda v', [] + l1, l2 = 'lambda v', [] for i in range(len(f)): - l = '%s,f%d=f[%d]' % (l, i, i) + l1 = '%s,f%d=f[%d]' % (l1, i, i) l2.append('f%d(v)' % (i)) - return eval('%s:%s' % (l, ' or '.join(l2))) + return eval('%s:%s' % (l1, ' or '.join(l2))) def l_not(f): @@ -666,7 +693,9 @@ def getcallprotoargument(rout, cb_map={}): pass else: ctype = ctype + '*' - if isstring(var) or isarrayofstrings(var): + if ((isstring(var) + or isarrayofstrings(var) # obsolete? + or isstringarray(var))): arg_types2.append('size_t') arg_types.append(ctype) @@ -731,14 +760,14 @@ def getrestdoc(rout): def gentitle(name): - l = (80 - len(name) - 6) // 2 - return '/*%s %s %s*/' % (l * '*', name, l * '*') + ln = (80 - len(name) - 6) // 2 + return '/*%s %s %s*/' % (ln * '*', name, ln * '*') -def flatlist(l): - if isinstance(l, list): - return reduce(lambda x, y, f=flatlist: x + f(y), l, []) - return [l] +def flatlist(lst): + if isinstance(lst, list): + return reduce(lambda x, y, f=flatlist: x + f(y), lst, []) + return [lst] def stripcomma(s): diff --git a/numpy/f2py/capi_maps.py b/numpy/f2py/capi_maps.py index 1f43207db..e5dc2331a 100644 --- a/numpy/f2py/capi_maps.py +++ b/numpy/f2py/capi_maps.py @@ -56,6 +56,7 @@ c2py_map = {'double': 'float', 'complex_double': 'complex', 'complex_long_double': 'complex', # forced casting 'string': 'string', + 'character': 'bytes', } c2capi_map = {'double': 'NPY_DOUBLE', 'float': 'NPY_FLOAT', @@ -72,7 +73,8 @@ c2capi_map = {'double': 'NPY_DOUBLE', 'complex_float': 'NPY_CFLOAT', 'complex_double': 'NPY_CDOUBLE', 'complex_long_double': 'NPY_CDOUBLE', # forced casting - 'string': 'NPY_STRING'} + 'string': 'NPY_STRING', + 'character': 'NPY_CHAR'} # These new maps aren't used anywhere yet, but should be by default # unless building numeric or numarray extensions. @@ -94,8 +96,8 @@ if using_newcore: 'complex_float': 'NPY_CFLOAT', 'complex_double': 'NPY_CDOUBLE', 'complex_long_double': 'NPY_CDOUBLE', - 'string':'NPY_STRING' - } + 'string': 'NPY_STRING', + 'character': 'NPY_STRING'} c2pycode_map = {'double': 'd', 'float': 'f', @@ -112,7 +114,8 @@ c2pycode_map = {'double': 'd', 'complex_float': 'F', 'complex_double': 'D', 'complex_long_double': 'D', # forced casting - 'string': 'c' + 'string': 'c', + 'character': 'c' } if using_newcore: @@ -133,8 +136,11 @@ if using_newcore: 'complex_float': 'F', 'complex_double': 'D', 'complex_long_double': 'G', - 'string': 'S'} + 'string': 'S', + 'character': 'c'} +# https://docs.python.org/3/c-api/arg.html#building-values +# c2buildvalue_map is NumPy agnostic, so no need to bother with using_newcore c2buildvalue_map = {'double': 'd', 'float': 'f', 'char': 'b', @@ -146,7 +152,8 @@ c2buildvalue_map = {'double': 'd', 'complex_float': 'N', 'complex_double': 'N', 'complex_long_double': 'N', - 'string': 'y'} + 'string': 'y', + 'character': 'c'} f2cmap_all = {'real': {'': 'float', '4': 'float', '8': 'double', '12': 'long_double', '16': 'long_double'}, @@ -165,7 +172,6 @@ f2cmap_all = {'real': {'': 'float', '4': 'float', '8': 'double', 'double complex': {'': 'complex_double'}, 'double precision': {'': 'double'}, 'byte': {'': 'char'}, - 'character': {'': 'string'} } f2cmap_default = copy.deepcopy(f2cmap_all) @@ -230,7 +236,8 @@ cformat_map = {'double': '%g', 'complex_float': '(%g,%g)', 'complex_double': '(%g,%g)', 'complex_long_double': '(%Lg,%Lg)', - 'string': '%s', + 'string': '\\"%s\\"', + 'character': "'%c'", } # Auxiliary functions @@ -252,6 +259,10 @@ def getctype(var): errmess('getctype: function %s has no return value?!\n' % a) elif issubroutine(var): return ctype + elif ischaracter_or_characterarray(var): + return 'character' + elif isstring_or_stringarray(var): + return 'string' elif 'typespec' in var and var['typespec'].lower() in f2cmap_all: typespec = var['typespec'].lower() f2cmap = f2cmap_all[typespec] @@ -283,6 +294,21 @@ def getctype(var): return ctype +def f2cexpr(expr): + """Rewrite Fortran expression as f2py supported C expression. + + Due to the lack of a proper expression parser in f2py, this + function uses a heuristic approach that assumes that Fortran + arithmetic expressions are valid C arithmetic expressions when + mapping Fortran function calls to the corresponding C function/CPP + macros calls. + + """ + # TODO: support Fortran `len` function with optional kind parameter + expr = re.sub(r'\blen\b', 'f2py_slen', expr) + return expr + + def getstrlength(var): if isstringfunction(var): if 'result' in var: @@ -302,7 +328,7 @@ def getstrlength(var): if '*' in a: len = a['*'] elif 'len' in a: - len = a['len'] + len = f2cexpr(a['len']) if re.match(r'\(\s*(\*|:)\s*\)', len) or re.match(r'(\*|:)', len): if isintent_hide(var): errmess('getstrlength:intent(hide): expected a string with defined length but got: %s\n' % ( @@ -499,6 +525,20 @@ def getinit(a, var): return init, showinit +def get_elsize(var): + if isstring(var) or isstringarray(var): + elsize = getstrlength(var) + # override with user-specified length when available: + elsize = var['charselector'].get('f2py_len', elsize) + return elsize + if ischaracter(var) or ischaracterarray(var): + return '1' + # for numerical types, PyArray_New* functions ignore specified + # elsize, so we just return 1 and let elsize be determined at + # runtime, see fortranobject.c + return '1' + + def sign2map(a, var): """ varname,ctype,atype @@ -553,6 +593,7 @@ def sign2map(a, var): dim = copy.copy(var['dimension']) if ret['ctype'] in c2capi_map: ret['atype'] = c2capi_map[ret['ctype']] + ret['elsize'] = get_elsize(var) # Debug info if debugcapi(var): il = [isintent_in, 'input', isintent_out, 'output', @@ -720,6 +761,7 @@ def cb_sign2map(a, var, index=None): ret['ctype'] = getctype(var) if ret['ctype'] in c2capi_map: ret['atype'] = c2capi_map[ret['ctype']] + ret['elsize'] = get_elsize(var) if ret['ctype'] in cformat_map: ret['showvalueformat'] = '%s' % (cformat_map[ret['ctype']]) if isarray(var): @@ -819,6 +861,7 @@ def common_sign2map(a, var): # obsolute ret['ctype'] = 'char' if ret['ctype'] in c2capi_map: ret['atype'] = c2capi_map[ret['ctype']] + ret['elsize'] = get_elsize(var) if ret['ctype'] in cformat_map: ret['showvalueformat'] = '%s' % (cformat_map[ret['ctype']]) if isarray(var): diff --git a/numpy/f2py/cb_rules.py b/numpy/f2py/cb_rules.py index c2d7faaad..761831e00 100644 --- a/numpy/f2py/cb_rules.py +++ b/numpy/f2py/cb_rules.py @@ -424,8 +424,11 @@ cb_arg_rules = [ {debugcapi: 'CFUNCSMESS'}, 'string.h'], '_check': l_and(isstring, isintent_out) }, { - 'pyobjfrom': [{debugcapi: ' fprintf(stderr,"debug-capi:cb:#varname#=\\"#showvalueformat#\\":%d:\\n",#varname_i#,#varname_i#_cb_len);'}, - {isintent_in: """\ + 'pyobjfrom': [ + {debugcapi: + (' fprintf(stderr,"debug-capi:cb:#varname#=#showvalueformat#:' + '%d:\\n",#varname_i#,#varname_i#_cb_len);')}, + {isintent_in: """\ if (cb->nofargs>capi_i) if (CAPI_ARGLIST_SETITEM(capi_i++,pyobj_from_#ctype#1size(#varname_i#,#varname_i#_cb_len))) goto capi_fail;"""}, @@ -451,15 +454,21 @@ cb_arg_rules = [ 'pyobjfrom': [{debugcapi: ' fprintf(stderr,"debug-capi:cb:#varname#\\n");'}, {isintent_c: """\ if (cb->nofargs>capi_i) { - int itemsize_ = #atype# == NPY_STRING ? 1 : 0; - /*XXX: Hmm, what will destroy this array??? */ - PyArrayObject *tmp_arr = (PyArrayObject *)PyArray_New(&PyArray_Type,#rank#,#varname_i#_Dims,#atype#,NULL,(char*)#varname_i#,itemsize_,NPY_ARRAY_CARRAY,NULL); + /* tmp_arr will be inserted to capi_arglist_list that will be + destroyed when leaving callback function wrapper together + with tmp_arr. */ + PyArrayObject *tmp_arr = (PyArrayObject *)PyArray_New(&PyArray_Type, + #rank#,#varname_i#_Dims,#atype#,NULL,(char*)#varname_i#,#elsize#, + NPY_ARRAY_CARRAY,NULL); """, l_not(isintent_c): """\ if (cb->nofargs>capi_i) { - int itemsize_ = #atype# == NPY_STRING ? 1 : 0; - /*XXX: Hmm, what will destroy this array??? */ - PyArrayObject *tmp_arr = (PyArrayObject *)PyArray_New(&PyArray_Type,#rank#,#varname_i#_Dims,#atype#,NULL,(char*)#varname_i#,itemsize_,NPY_ARRAY_FARRAY,NULL); + /* tmp_arr will be inserted to capi_arglist_list that will be + destroyed when leaving callback function wrapper together + with tmp_arr. */ + PyArrayObject *tmp_arr = (PyArrayObject *)PyArray_New(&PyArray_Type, + #rank#,#varname_i#_Dims,#atype#,NULL,(char*)#varname_i#,#elsize#, + NPY_ARRAY_FARRAY,NULL); """, }, """ diff --git a/numpy/f2py/cfuncs.py b/numpy/f2py/cfuncs.py index f69933543..d644ede8c 100644 --- a/numpy/f2py/cfuncs.py +++ b/numpy/f2py/cfuncs.py @@ -88,6 +88,7 @@ typedefs[ typedefs['complex_float'] = 'typedef struct {float r,i;} complex_float;' typedefs['complex_double'] = 'typedef struct {double r,i;} complex_double;' typedefs['string'] = """typedef char * string;""" +typedefs['character'] = """typedef char character;""" ############### CPP macros #################### @@ -242,47 +243,16 @@ cppmacros['MINMAX'] = """\ #define MIN(a,b) ((a < b) ? (a) : (b)) #endif """ -needs['len..'] = ['f2py_size'] cppmacros['len..'] = """\ -#define rank(var) var ## _Rank -#define shape(var,dim) var ## _Dims[dim] -#define old_rank(var) (PyArray_NDIM((PyArrayObject *)(capi_ ## var ## _tmp))) -#define old_shape(var,dim) PyArray_DIM(((PyArrayObject *)(capi_ ## var ## _tmp)),dim) -#define fshape(var,dim) shape(var,rank(var)-dim-1) -#define len(var) shape(var,0) -#define flen(var) fshape(var,0) -#define old_size(var) PyArray_SIZE((PyArrayObject *)(capi_ ## var ## _tmp)) -/* #define index(i) capi_i ## i */ -#define slen(var) capi_ ## var ## _len -#define size(var, ...) f2py_size((PyArrayObject *)(capi_ ## var ## _tmp), ## __VA_ARGS__, -1) +/* See fortranobject.h for definitions. The macros here are provided for BC. */ +#define rank f2py_rank +#define shape f2py_shape +#define fshape f2py_shape +#define len f2py_len +#define flen f2py_flen +#define slen f2py_slen +#define size f2py_size """ -needs['f2py_size'] = ['stdarg.h'] -cfuncs['f2py_size'] = """\ -static int f2py_size(PyArrayObject* var, ...) -{ - npy_int sz = 0; - npy_int dim; - npy_int rank; - va_list argp; - va_start(argp, var); - dim = va_arg(argp, npy_int); - if (dim==-1) - { - sz = PyArray_SIZE(var); - } - else - { - rank = PyArray_NDIM(var); - if (dim>=1 && dim<=rank) - sz = PyArray_DIM(var, dim-1); - else - fprintf(stderr, \"f2py_size: 2nd argument value=%d fails to satisfy 1<=value<=%d. Result will be 0.\\n\", dim, rank); - } - va_end(argp); - return sz; -} -""" - cppmacros[ 'pyobj_from_char1'] = '#define pyobj_from_char1(v) (PyLong_FromLong(v))' cppmacros[ @@ -785,6 +755,66 @@ capi_fail: } """ +cfuncs['character_from_pyobj'] = """\ +static int +character_from_pyobj(character* v, PyObject *obj, const char *errmess) { + if (PyBytes_Check(obj)) { + /* empty bytes has trailing null, so dereferencing is always safe */ + *v = PyBytes_AS_STRING(obj)[0]; + return 1; + } else if (PyUnicode_Check(obj)) { + PyObject* tmp = PyUnicode_AsASCIIString(obj); + if (tmp != NULL) { + *v = PyBytes_AS_STRING(tmp)[0]; + Py_DECREF(tmp); + return 1; + } + } else if (PyArray_Check(obj)) { + PyArrayObject* arr = (PyArrayObject*)obj; + if (F2PY_ARRAY_IS_CHARACTER_COMPATIBLE(arr)) { + *v = PyArray_BYTES(arr)[0]; + return 1; + } else if (F2PY_IS_UNICODE_ARRAY(arr)) { + // TODO: update when numpy will support 1-byte and + // 2-byte unicode dtypes + PyObject* tmp = PyUnicode_FromKindAndData( + PyUnicode_4BYTE_KIND, + PyArray_BYTES(arr), + (PyArray_NBYTES(arr)>0?1:0)); + if (tmp != NULL) { + if (character_from_pyobj(v, tmp, errmess)) { + Py_DECREF(tmp); + return 1; + } + Py_DECREF(tmp); + } + } + } else if (PySequence_Check(obj)) { + PyObject* tmp = PySequence_GetItem(obj,0); + if (tmp != NULL) { + if (character_from_pyobj(v, tmp, errmess)) { + Py_DECREF(tmp); + return 1; + } + Py_DECREF(tmp); + } + } + { + char mess[F2PY_MESSAGE_BUFFER_SIZE]; + strcpy(mess, errmess); + PyObject* err = PyErr_Occurred(); + if (err == NULL) { + err = PyExc_TypeError; + } + sprintf(mess + strlen(mess), + " -- expected str|bytes|sequence-of-str-or-bytes, got "); + f2py_describe(obj, mess + strlen(mess)); + PyErr_SetString(err, mess); + } + return 0; +} +""" + needs['char_from_pyobj'] = ['int_from_pyobj'] cfuncs['char_from_pyobj'] = """\ static int @@ -1179,6 +1209,31 @@ complex_float_from_pyobj(complex_float* v,PyObject *obj,const char *errmess) """ +cfuncs['try_pyarr_from_character'] = """\ +static int try_pyarr_from_character(PyObject* obj, character* v) { + PyArrayObject *arr = (PyArrayObject*)obj; + if (!obj) return -2; + if (PyArray_Check(obj)) { + if (F2PY_ARRAY_IS_CHARACTER_COMPATIBLE(arr)) { + *(character *)(PyArray_DATA(arr)) = *v; + return 1; + } + } + { + char mess[F2PY_MESSAGE_BUFFER_SIZE]; + PyObject* err = PyErr_Occurred(); + if (err == NULL) { + err = PyExc_ValueError; + strcpy(mess, "try_pyarr_from_character failed" + " -- expected bytes array-scalar|array, got "); + f2py_describe(obj, mess + strlen(mess)); + } + PyErr_SetString(err, mess); + } + return 0; +} +""" + needs['try_pyarr_from_char'] = ['pyobj_from_char1', 'TRYPYARRAYTEMPLATE'] cfuncs[ 'try_pyarr_from_char'] = 'static int try_pyarr_from_char(PyObject* obj,char* v) {\n TRYPYARRAYTEMPLATE(char,\'c\');\n}\n' diff --git a/numpy/f2py/common_rules.py b/numpy/f2py/common_rules.py index 937d8bc72..5a488bf5a 100644 --- a/numpy/f2py/common_rules.py +++ b/numpy/f2py/common_rules.py @@ -91,6 +91,7 @@ def buildhooks(m): idims = [] for n in inames: ct = capi_maps.getctype(vars[n]) + elsize = capi_maps.get_elsize(vars[n]) at = capi_maps.c2capi_map[ct] dm = capi_maps.getarrdims(n, vars[n]) if dm['dims']: @@ -100,7 +101,8 @@ def buildhooks(m): dms = dm['dims'].strip() if not dms: dms = '-1' - cadd('\t{\"%s\",%s,{{%s}},%s},' % (n, dm['rank'], dms, at)) + cadd('\t{\"%s\",%s,{{%s}},%s, %s},' + % (n, dm['rank'], dms, at, elsize)) cadd('\t{NULL}\n};') inames1 = rmbadname(inames) inames1_tps = ','.join(['char *' + s for s in inames1]) @@ -121,7 +123,9 @@ def buildhooks(m): % (F_FUNC, lower_name, name.upper(), name)) cadd('}\n') iadd('\ttmp = PyFortranObject_New(f2py_%s_def,f2py_init_%s);' % (name, name)) - iadd('\tF2PyDict_SetItemString(d, \"%s\", tmp);' % name) + iadd('\tif (tmp == NULL) return NULL;') + iadd('\tif (F2PyDict_SetItemString(d, \"%s\", tmp) == -1) return NULL;' + % name) iadd('\tPy_DECREF(tmp);') tname = name.replace('_', '\\_') dadd('\\subsection{Common block \\texttt{%s}}\n' % (tname)) diff --git a/numpy/f2py/crackfortran.py b/numpy/f2py/crackfortran.py index 515bdd787..cfd58dfed 100755 --- a/numpy/f2py/crackfortran.py +++ b/numpy/f2py/crackfortran.py @@ -84,7 +84,7 @@ Usage: 'optional','required', etc) K = D['kindselector'] = {['*','kind']} (only if D['typespec'] = 'complex' | 'integer' | 'logical' | 'real' ) - C = D['charselector'] = {['*','len','kind']} + C = D['charselector'] = {['*','len','kind','f2py_len']} (only if D['typespec']=='character') D['='] --- initialization expression string D['typename'] --- name of the type if D['typespec']=='type' @@ -97,7 +97,7 @@ Usage: D['typespec>']*K['*'] D['typespec'](kind=K['kind']) character*C['*'] - character(len=C['len'],kind=C['kind']) + character(len=C['len'],kind=C['kind'], f2py_len=C['f2py_len']) (see also fortran type declaration statement formats below) Fortran 90 type declaration statement format (F77 is subset of F90) @@ -146,6 +146,11 @@ import re import os import copy import platform +import codecs +try: + import chardet +except ImportError: + chardet = None from . import __version__ @@ -301,12 +306,38 @@ _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 +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. + """ + 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' + else: + if chardet is not None: + encoding = chardet.detect(raw)['encoding'] + else: + # hint: install chardet to ensure correct encoding handling + encoding = 'ascii' + return open(filename, mode, encoding=encoding) + + def is_free_format(file): """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 open(file, 'r') as f: + with openhook(file, 'r') as f: line = f.readline() n = 15 # the number of non-comment lines to scan for hints if _has_f_header(line): @@ -356,9 +387,16 @@ def readfortrancode(ffile, dowithline=show, istop=1): ll, l1 = '', '' spacedigits = [' '] + [str(_m) for _m in range(10)] filepositiontext = '' - fin = fileinput.FileInput(ffile) + fin = fileinput.FileInput(ffile, openhook=openhook) while True: - l = fin.readline() + try: + l = fin.readline() + except UnicodeDecodeError as msg: + raise Exception( + f'readfortrancode: reading {fin.filename()}#{fin.lineno()}' + f' failed with\n{msg}.\nIt is likely that installing chardet' + ' package will help f2py determine the input file encoding' + ' correctly.') if not l: break if fin.isfirstline(): @@ -1546,7 +1584,9 @@ kindselector = re.compile( charselector = re.compile( r'\s*(\((?P<lenkind>.*)\)|\*\s*(?P<charlen>.*))\s*\Z', re.I) lenkindpattern = re.compile( - r'\s*(kind\s*=\s*(?P<kind>.*?)\s*(@,@\s*len\s*=\s*(?P<len>.*)|)|(len\s*=\s*|)(?P<len2>.*?)\s*(@,@\s*(kind\s*=\s*|)(?P<kind2>.*)|))\s*\Z', re.I) + r'\s*(kind\s*=\s*(?P<kind>.*?)\s*(@,@\s*len\s*=\s*(?P<len>.*)|)' + r'|(len\s*=\s*|)(?P<len2>.*?)\s*(@,@\s*(kind\s*=\s*|)(?P<kind2>.*)' + r'|(f2py_len\s*=\s*(?P<f2py_len>.*))|))\s*\Z', re.I) lenarraypattern = re.compile( r'\s*(@\(@\s*(?!/)\s*(?P<array>.*?)\s*@\)@\s*\*\s*(?P<len>.*?)|(\*\s*(?P<len2>.*?)|)\s*(@\(@\s*(?!/)\s*(?P<array2>.*?)\s*@\)@|))\s*(=\s*(?P<init>.*?)|(@\(@|)/\s*(?P<init2>.*?)\s*/(@\)@|)|)\s*\Z', re.I) @@ -1788,6 +1828,9 @@ def cracktypespec(typespec, selector): lenkind[lk] = lenkind[lk + '2'] charselect[lk] = lenkind[lk] del lenkind[lk + '2'] + if lenkind['f2py_len'] is not None: + # used to specify the length of assumed length strings + charselect['f2py_len'] = lenkind['f2py_len'] del charselect['lenkind'] for k in list(charselect.keys()): if not charselect[k]: @@ -1857,6 +1900,7 @@ def setcharselector(decl, sel, force=0): if 'charselector' not in decl: decl['charselector'] = sel return decl + for k in list(sel.keys()): if force or k not in decl['charselector']: decl['charselector'][k] = sel[k] @@ -2465,6 +2509,7 @@ def _eval_scalar(value, params): if _is_kind_number(value): value = value.split('_')[0] try: + # TODO: use symbolic from PR #19805 value = eval(value, {}, params) value = (repr if isinstance(value, str) else str)(value) except (NameError, SyntaxError, TypeError): @@ -2534,7 +2579,6 @@ def analyzevars(block): elif n in block['args']: outmess('analyzevars: typespec of variable %s is not defined in routine %s.\n' % ( repr(n), block['name'])) - if 'charselector' in vars[n]: if 'len' in vars[n]['charselector']: l = vars[n]['charselector']['len'] @@ -2667,26 +2711,6 @@ def analyzevars(block): dimension_exprs[d] = solver_and_deps vars[n]['dimension'].append(d) - if 'dimension' in vars[n]: - if isstringarray(vars[n]): - if 'charselector' in vars[n]: - d = vars[n]['charselector'] - if '*' in d: - d = d['*'] - errmess('analyzevars: character array "character*%s %s(%s)" is considered as "character %s(%s)"; "intent(c)" is forced.\n' - % (d, n, - ','.join(vars[n]['dimension']), - n, ','.join(vars[n]['dimension'] + [d]))) - vars[n]['dimension'].append(d) - del vars[n]['charselector'] - if 'intent' not in vars[n]: - vars[n]['intent'] = [] - if 'c' not in vars[n]['intent']: - vars[n]['intent'].append('c') - else: - errmess( - "analyzevars: charselector=%r unhandled.\n" % (d)) - if 'check' not in vars[n] and 'args' in block and n in block['args']: # n is an argument that has no checks defined. Here we # generate some consistency checks for n, and when n is an @@ -3220,6 +3244,13 @@ def vars2fortran(block, vars, args, tab='', as_interface=False): if 'attrspec' in vars[a]: attr = [l for l in vars[a]['attrspec'] if l not in ['external']] + if as_interface and 'intent(in)' in attr and 'intent(out)' in attr: + # In Fortran, intent(in, out) are conflicting while + # intent(in, out) can be specified only via + # `!f2py intent(out) ..`. + # So, for the Fortran interface, we'll drop + # intent(out) to resolve the conflict. + attr.remove('intent(out)') if attr: vardef = '%s, %s' % (vardef, ','.join(attr)) c = ',' @@ -3255,14 +3286,23 @@ def vars2fortran(block, vars, args, tab='', as_interface=False): ###### +# We expose post_processing_hooks as global variable so that +# user-libraries could register their own hooks to f2py. +post_processing_hooks = [] + + def crackfortran(files): - global usermodules + global usermodules, post_processing_hooks outmess('Reading fortran codes...\n', 0) readfortrancode(files, crackline) outmess('Post-processing...\n', 0) usermodules = [] postlist = postcrack(grouplist[0]) + outmess('Applying post-processing hooks...\n', 0) + for hook in post_processing_hooks: + outmess(f' {hook.__name__}\n', 0) + postlist = traverse(postlist, hook) outmess('Post-processing (stage 2)...\n', 0) postlist = postcrack2(postlist) return usermodules + postlist @@ -3282,6 +3322,142 @@ def crack2fortran(block): """ % (f2py_version) return header + pyf + footer + +def _is_visit_pair(obj): + return (isinstance(obj, tuple) + and len(obj) == 2 + and isinstance(obj[0], (int, str))) + + +def traverse(obj, visit, parents=[], result=None, *args, **kwargs): + '''Traverse f2py data structure with the following visit function: + + def visit(item, parents, result, *args, **kwargs): + """ + + parents is a list of key-"f2py data structure" pairs from which + items are taken from. + + result is a f2py data structure that is filled with the + return value of the visit function. + + item is 2-tuple (index, value) if parents[-1][1] is a list + item is 2-tuple (key, value) if parents[-1][1] is a dict + + The return value of visit must be None, or of the same kind as + item, that is, if parents[-1] is a list, the return value must + be 2-tuple (new_index, new_value), or if parents[-1] is a + dict, the return value must be 2-tuple (new_key, new_value). + + If new_index or new_value is None, the return value of visit + is ignored, that is, it will not be added to the result. + + If the return value is None, the content of obj will be + traversed, otherwise not. + """ + ''' + + if _is_visit_pair(obj): + if obj[0] == 'parent_block': + # avoid infinite recursion + return obj + new_result = visit(obj, parents, result, *args, **kwargs) + if new_result is not None: + assert _is_visit_pair(new_result) + return new_result + parent = obj + result_key, obj = obj + else: + parent = (None, obj) + result_key = None + + if isinstance(obj, list): + new_result = [] + for index, value in enumerate(obj): + new_index, new_item = traverse((index, value), visit, + parents=parents + [parent], + result=result, *args, **kwargs) + if new_index is not None: + new_result.append(new_item) + elif isinstance(obj, dict): + new_result = dict() + for key, value in obj.items(): + new_key, new_value = traverse((key, value), visit, + parents=parents + [parent], + result=result, *args, **kwargs) + if new_key is not None: + new_result[new_key] = new_value + else: + new_result = obj + + if result_key is None: + return new_result + return result_key, new_result + + +def character_backward_compatibility_hook(item, parents, result, + *args, **kwargs): + """Previously, Fortran character was incorrectly treated as + character*1. This hook fixes the usage of the corresponding + variables in `check`, `dimension`, `=`, and `callstatement` + expressions. + + The usage of `char*` in `callprotoargument` expression can be left + unchanged because C `character` is C typedef of `char`, although, + new implementations should use `character*` in the corresponding + expressions. + + See https://github.com/numpy/numpy/pull/19388 for more information. + + """ + parent_key, parent_value = parents[-1] + key, value = item + + def fix_usage(varname, value): + value = re.sub(r'[*]\s*\b' + varname + r'\b', varname, value) + value = re.sub(r'\b' + varname + r'\b\s*[\[]\s*0\s*[\]]', + varname, value) + return value + + if parent_key in ['dimension', 'check']: + assert parents[-3][0] == 'vars' + vars_dict = parents[-3][1] + elif key == '=': + assert parents[-2][0] == 'vars' + vars_dict = parents[-2][1] + else: + vars_dict = None + + new_value = None + if vars_dict is not None: + new_value = value + for varname, vd in vars_dict.items(): + if ischaracter(vd): + new_value = fix_usage(varname, new_value) + elif key == 'callstatement': + vars_dict = parents[-2][1]['vars'] + new_value = value + for varname, vd in vars_dict.items(): + if ischaracter(vd): + # replace all occurrences of `<varname>` with + # `&<varname>` in argument passing + new_value = re.sub( + r'(?<![&])\b' + varname + r'\b', '&' + varname, new_value) + + if new_value is not None: + if new_value != value: + # We report the replacements here so that downstream + # software could update their source codes + # accordingly. However, such updates are recommended only + # when BC with numpy 1.21 or older is not required. + outmess(f'character_bc_hook[{parent_key}.{key}]:' + f' replaced `{value}` -> `{new_value}`\n', 1) + return (key, new_value) + + +post_processing_hooks.append(character_backward_compatibility_hook) + + if __name__ == "__main__": files = [] funcs = [] @@ -3341,17 +3517,18 @@ if __name__ == "__main__": funcs.append(l) if not strictf77 and f77modulename and not skipemptyends: outmess("""\ - Warning: You have specified module name for non Fortran 77 code - that should not need one (expect if you are scanning F90 code - for non module blocks but then you should use flag -skipemptyends - and also be sure that the files do not contain programs without program statement). + Warning: You have specified module name for non Fortran 77 code that + should not need one (expect if you are scanning F90 code for non + module blocks but then you should use flag -skipemptyends and also + be sure that the files do not contain programs without program + statement). """, 0) postlist = crackfortran(files) if pyffilename: outmess('Writing fortran code to file %s\n' % repr(pyffilename), 0) pyf = crack2fortran(postlist) - with open(pyffilename, 'w') as f: + with open(pyffilename, 'w') as f: f.write(pyf) if showblocklist: show(postlist) diff --git a/numpy/f2py/f90mod_rules.py b/numpy/f2py/f90mod_rules.py index 3e1c9674f..a3bb6a212 100644 --- a/numpy/f2py/f90mod_rules.py +++ b/numpy/f2py/f90mod_rules.py @@ -147,17 +147,9 @@ def buildhooks(pymod): if not dms: dms = '-1' use_fgetdims2 = fgetdims2 - if isstringarray(var): - if 'charselector' in var and 'len' in var['charselector']: - cadd('\t{"%s",%s,{{%s,%s}},%s},' - % (undo_rmbadname1(n), dm['rank'], dms, var['charselector']['len'], at)) - use_fgetdims2 = fgetdims2_sa - else: - cadd('\t{"%s",%s,{{%s}},%s},' % - (undo_rmbadname1(n), dm['rank'], dms, at)) - else: - cadd('\t{"%s",%s,{{%s}},%s},' % - (undo_rmbadname1(n), dm['rank'], dms, at)) + cadd('\t{"%s",%s,{{%s}},%s, %s},' % + (undo_rmbadname1(n), dm['rank'], dms, at, + capi_maps.get_elsize(var))) dadd('\\item[]{{}\\verb@%s@{}}' % (capi_maps.getarrdocsign(n, var))) if hasnote(var): @@ -216,8 +208,10 @@ def buildhooks(pymod): ar['docs'] = [] ar['docshort'] = [] ret = dictappend(ret, ar) - cadd('\t{"%s",-1,{{-1}},0,NULL,(void *)f2py_rout_#modulename#_%s_%s,doc_f2py_rout_#modulename#_%s_%s},' % - (b['name'], m['name'], b['name'], m['name'], b['name'])) + cadd(('\t{"%s",-1,{{-1}},0,0,NULL,(void *)' + 'f2py_rout_#modulename#_%s_%s,' + 'doc_f2py_rout_#modulename#_%s_%s},') + % (b['name'], m['name'], b['name'], m['name'], b['name'])) sargs.append('char *%s' % (b['name'])) sargsp.append('char *') iadd('\tf2py_%s_def[i_f2py++].data = %s;' % diff --git a/numpy/f2py/func2subr.py b/numpy/f2py/func2subr.py index 21d4c009c..2a05f065b 100644 --- a/numpy/f2py/func2subr.py +++ b/numpy/f2py/func2subr.py @@ -13,10 +13,6 @@ $Date: 2004/11/26 11:13:06 $ Pearu Peterson """ -__version__ = "$Revision: 1.16 $"[10:-1] - -f2py_version = 'See `f2py -v`' - import copy from .auxfuncs import ( @@ -108,15 +104,19 @@ def createfuncwrapper(rout, signature=0): else: args = [newname] + rout['args'] - l = var2fixfortran(vars, name, newname, f90mode) - if l[:13] == 'character*(*)': + l_tmpl = var2fixfortran(vars, name, '@@@NAME@@@', f90mode) + if l_tmpl[:13] == 'character*(*)': if f90mode: - l = 'character(len=10)' + l[13:] + l_tmpl = 'character(len=10)' + l_tmpl[13:] else: - l = 'character*10' + l[13:] + l_tmpl = 'character*10' + l_tmpl[13:] charselect = vars[name]['charselector'] if charselect.get('*', '') == '(*)': charselect['*'] = '10' + + l1 = l_tmpl.replace('@@@NAME@@@', newname) + rl = None + sargs = ', '.join(args) if f90mode: add('subroutine f2pywrap_%s_%s (%s)' % @@ -127,7 +127,8 @@ def createfuncwrapper(rout, signature=0): add('subroutine f2pywrap%s (%s)' % (name, sargs)) if not need_interface: add('external %s' % (fortranname)) - l = l + ', ' + fortranname + rl = l_tmpl.replace('@@@NAME@@@', '') + ' ' + fortranname + if need_interface: for line in rout['saved_interface'].split('\n'): if line.lstrip().startswith('use ') and '__user__' not in line: @@ -156,7 +157,9 @@ def createfuncwrapper(rout, signature=0): continue add(var2fixfortran(vars, a, f90mode=f90mode)) - add(l) + add(l1) + if rl is not None: + add(rl) if need_interface: if f90mode: @@ -293,8 +296,8 @@ def assubr(rout): if issubroutine_wrap(rout): fortranname = getfortranname(rout) name = rout['name'] - outmess('\t\tCreating wrapper for Fortran subroutine "%s"("%s")...\n' % ( - name, fortranname)) + outmess('\t\tCreating wrapper for Fortran subroutine "%s"("%s")...\n' + % (name, fortranname)) rout = copy.copy(rout) return rout, createsubrwrapper(rout) return rout, '' diff --git a/numpy/f2py/rules.py b/numpy/f2py/rules.py index c56225032..c9c3b2383 100755 --- a/numpy/f2py/rules.py +++ b/numpy/f2py/rules.py @@ -57,22 +57,23 @@ from pathlib import Path # __version__.version is now the same as the NumPy version from . import __version__ -f2py_version = __version__.version -numpy_version = __version__.version from .auxfuncs import ( applyrules, debugcapi, dictappend, errmess, gentitle, getargs2, - hascallstatement, hasexternals, hasinitvalue, hasnote, hasresultnote, - isarray, isarrayofstrings, iscomplex, iscomplexarray, - iscomplexfunction, iscomplexfunction_warn, isdummyroutine, isexternal, - isfunction, isfunction_wrap, isint1array, isintent_aux, isintent_c, - isintent_callback, isintent_copy, isintent_hide, isintent_inout, - isintent_nothide, isintent_out, isintent_overwrite, islogical, - islong_complex, islong_double, islong_doublefunction, islong_long, - islong_longfunction, ismoduleroutine, isoptional, isrequired, isscalar, - issigned_long_longarray, isstring, isstringarray, isstringfunction, - issubroutine, issubroutine_wrap, isthreadsafe, isunsigned, - isunsigned_char, isunsigned_chararray, isunsigned_long_long, + hascallstatement, hasexternals, hasinitvalue, hasnote, + hasresultnote, isarray, isarrayofstrings, ischaracter, + ischaracterarray, ischaracter_or_characterarray, iscomplex, + iscomplexarray, iscomplexfunction, iscomplexfunction_warn, + isdummyroutine, isexternal, isfunction, isfunction_wrap, isint1, + isint1array, isintent_aux, isintent_c, isintent_callback, + isintent_copy, isintent_hide, isintent_inout, isintent_nothide, + isintent_out, isintent_overwrite, islogical, islong_complex, + islong_double, islong_doublefunction, islong_long, + islong_longfunction, ismoduleroutine, isoptional, isrequired, + isscalar, issigned_long_longarray, isstring, isstringarray, + isstringfunction, issubroutine, + issubroutine_wrap, isthreadsafe, isunsigned, isunsigned_char, + isunsigned_chararray, isunsigned_long_long, isunsigned_long_longarray, isunsigned_short, isunsigned_shortarray, l_and, l_not, l_or, outmess, replace, stripcomma, requiresf90wrapper ) @@ -84,9 +85,12 @@ from . import use_rules from . import f90mod_rules from . import func2subr +f2py_version = __version__.version +numpy_version = __version__.version + options = {} sepdict = {} -#for k in ['need_cfuncs']: sepdict[k]=',' +# for k in ['need_cfuncs']: sepdict[k]=',' for k in ['decl', 'frompyobj', 'cleanupfrompyobj', @@ -405,10 +409,18 @@ rout_rules = [ ismoduleroutine: '', isdummyroutine: '' }, - 'routine_def': {l_not(l_or(ismoduleroutine, isintent_c, isdummyroutine)): ' {\"#name#\",-1,{{-1}},0,(char *)#F_FUNC#(#fortranname#,#FORTRANNAME#),(f2py_init_func)#apiname#,doc_#apiname#},', - l_and(l_not(ismoduleroutine), isintent_c, l_not(isdummyroutine)): ' {\"#name#\",-1,{{-1}},0,(char *)#fortranname#,(f2py_init_func)#apiname#,doc_#apiname#},', - l_and(l_not(ismoduleroutine), isdummyroutine): ' {\"#name#\",-1,{{-1}},0,NULL,(f2py_init_func)#apiname#,doc_#apiname#},', - }, + 'routine_def': { + l_not(l_or(ismoduleroutine, isintent_c, isdummyroutine)): + ' {\"#name#\",-1,{{-1}},0,0,(char *)' + ' #F_FUNC#(#fortranname#,#FORTRANNAME#),' + ' (f2py_init_func)#apiname#,doc_#apiname#},', + l_and(l_not(ismoduleroutine), isintent_c, l_not(isdummyroutine)): + ' {\"#name#\",-1,{{-1}},0,0,(char *)#fortranname#,' + ' (f2py_init_func)#apiname#,doc_#apiname#},', + l_and(l_not(ismoduleroutine), isdummyroutine): + ' {\"#name#\",-1,{{-1}},0,0,NULL,' + ' (f2py_init_func)#apiname#,doc_#apiname#},', + }, 'need': {l_and(l_not(l_or(ismoduleroutine, isintent_c)), l_not(isdummyroutine)): 'F_FUNC'}, 'callfortranroutine': [ {debugcapi: [ @@ -432,9 +444,15 @@ rout_rules = [ isdummyroutine: '', }, - 'routine_def': {l_not(l_or(ismoduleroutine, isdummyroutine)): ' {\"#name#\",-1,{{-1}},0,(char *)#F_WRAPPEDFUNC#(#name_lower#,#NAME#),(f2py_init_func)#apiname#,doc_#apiname#},', - isdummyroutine: ' {\"#name#\",-1,{{-1}},0,NULL,(f2py_init_func)#apiname#,doc_#apiname#},', - }, + 'routine_def': { + l_not(l_or(ismoduleroutine, isdummyroutine)): + ' {\"#name#\",-1,{{-1}},0,0,(char *)' + ' #F_WRAPPEDFUNC#(#name_lower#,#NAME#),' + ' (f2py_init_func)#apiname#,doc_#apiname#},', + isdummyroutine: + ' {\"#name#\",-1,{{-1}},0,0,NULL,' + ' (f2py_init_func)#apiname#,doc_#apiname#},', + }, 'initf2pywraphook': {l_not(l_or(ismoduleroutine, isdummyroutine)): ''' { extern #ctype# #F_FUNC#(#name_lower#,#NAME#)(void); @@ -470,9 +488,15 @@ rout_rules = [ isdummyroutine: '', }, - 'routine_def': {l_not(l_or(ismoduleroutine, isdummyroutine)): ' {\"#name#\",-1,{{-1}},0,(char *)#F_WRAPPEDFUNC#(#name_lower#,#NAME#),(f2py_init_func)#apiname#,doc_#apiname#},', - isdummyroutine: ' {\"#name#\",-1,{{-1}},0,NULL,(f2py_init_func)#apiname#,doc_#apiname#},', - }, + 'routine_def': { + l_not(l_or(ismoduleroutine, isdummyroutine)): + ' {\"#name#\",-1,{{-1}},0,0,(char *)' + ' #F_WRAPPEDFUNC#(#name_lower#,#NAME#),' + ' (f2py_init_func)#apiname#,doc_#apiname#},', + isdummyroutine: + ' {\"#name#\",-1,{{-1}},0,0,NULL,' + ' (f2py_init_func)#apiname#,doc_#apiname#},', + }, 'initf2pywraphook': {l_not(l_or(ismoduleroutine, isdummyroutine)): ''' { extern void #F_FUNC#(#name_lower#,#NAME#)(void); @@ -525,10 +549,19 @@ rout_rules = [ l_and(l_not(ismoduleroutine), isintent_c, l_not(isdummyroutine)): 'extern #ctype# #fortranname#(#callprotoargument#);', isdummyroutine: '' }, - 'routine_def': {l_and(l_not(l_or(ismoduleroutine, isintent_c)), l_not(isdummyroutine)): ' {\"#name#\",-1,{{-1}},0,(char *)#F_FUNC#(#fortranname#,#FORTRANNAME#),(f2py_init_func)#apiname#,doc_#apiname#},', - l_and(l_not(ismoduleroutine), isintent_c, l_not(isdummyroutine)): ' {\"#name#\",-1,{{-1}},0,(char *)#fortranname#,(f2py_init_func)#apiname#,doc_#apiname#},', - isdummyroutine: ' {\"#name#\",-1,{{-1}},0,NULL,(f2py_init_func)#apiname#,doc_#apiname#},', - }, + 'routine_def': { + l_and(l_not(l_or(ismoduleroutine, isintent_c)), + l_not(isdummyroutine)): + (' {\"#name#\",-1,{{-1}},0,0,(char *)' + ' #F_FUNC#(#fortranname#,#FORTRANNAME#),' + ' (f2py_init_func)#apiname#,doc_#apiname#},'), + l_and(l_not(ismoduleroutine), isintent_c, l_not(isdummyroutine)): + (' {\"#name#\",-1,{{-1}},0,0,(char *)#fortranname#,' + ' (f2py_init_func)#apiname#,doc_#apiname#},'), + isdummyroutine: + ' {\"#name#\",-1,{{-1}},0,0,NULL,' + '(f2py_init_func)#apiname#,doc_#apiname#},', + }, 'decl': [{iscomplexfunction_warn: ' #ctype# #name#_return_value={0,0};', l_not(iscomplexfunction): ' #ctype# #name#_return_value=0;'}, {iscomplexfunction: @@ -562,9 +595,9 @@ rout_rules = [ }, { # String function # in use for --no-wrap 'declfortranroutine': 'extern void #F_FUNC#(#fortranname#,#FORTRANNAME#)(#callprotoargument#);', 'routine_def': {l_not(l_or(ismoduleroutine, isintent_c)): - ' {\"#name#\",-1,{{-1}},0,(char *)#F_FUNC#(#fortranname#,#FORTRANNAME#),(f2py_init_func)#apiname#,doc_#apiname#},', + ' {\"#name#\",-1,{{-1}},0,0,(char *)#F_FUNC#(#fortranname#,#FORTRANNAME#),(f2py_init_func)#apiname#,doc_#apiname#},', l_and(l_not(ismoduleroutine), isintent_c): - ' {\"#name#\",-1,{{-1}},0,(char *)#fortranname#,(f2py_init_func)#apiname#,doc_#apiname#},' + ' {\"#name#\",-1,{{-1}},0,0,(char *)#fortranname#,(f2py_init_func)#apiname#,doc_#apiname#},' }, 'decl': [' #ctype# #name#_return_value = NULL;', ' int #name#_return_value_len = 0;'], @@ -623,6 +656,8 @@ typedef_need_dict = {islong_long: 'long_long', isunsigned_shortarray: 'unsigned_short', isunsigned_long_longarray: 'unsigned_long_long', issigned_long_longarray: 'long_long', + isint1: 'signed_char', + ischaracter_or_characterarray: 'character', } aux_rules = [ @@ -685,7 +720,7 @@ aux_rules = [ }, # Integer*-1 array {'need': '#ctype#', - '_check': isunsigned_chararray, + '_check': l_or(isunsigned_chararray, isunsigned_char), '_depend': '' }, # Integer*-2 array @@ -856,7 +891,8 @@ if (#varname#_cb.capi==Py_None) { if (f2py_success) {"""}, 'closepyobjfrom': {isintent_inout: " } /*if (f2py_success) of #varname# pyobjfrom*/"}, 'need': {isintent_inout: 'try_pyarr_from_#ctype#'}, - '_check': l_and(isscalar, l_not(iscomplex), isintent_nothide) + '_check': l_and(isscalar, l_not(iscomplex), l_not(isstring), + isintent_nothide) }, { 'frompyobj': [ # hasinitvalue... @@ -963,11 +999,11 @@ if (#varname#_cb.capi==Py_None) { 'return': {isintent_out: ',#varname#'}, 'need': ['len..', {l_and(isintent_out, l_not(isintent_c)): 'STRINGPADN'}], - '_check':isstring + '_check': isstring }, { # Common 'frompyobj': [ """\ - slen(#varname#) = #length#; + slen(#varname#) = #elsize#; f2py_success = #ctype#_from_pyobj(&#varname#,&slen(#varname#),#init#,""" """#varname#_capi,\"#ctype#_from_pyobj failed in converting #nth#""" """`#varname#\' of #pyname# to C #ctype#\"); @@ -1011,11 +1047,13 @@ if (#varname#_cb.capi==Py_None) { 'decl': [' #ctype# *#varname# = NULL;', ' npy_intp #varname#_Dims[#rank#] = {#rank*[-1]#};', ' const int #varname#_Rank = #rank#;', - ' PyArrayObject *capi_#varname#_tmp = NULL;', + ' PyArrayObject *capi_#varname#_as_array = NULL;', ' int capi_#varname#_intent = 0;', + {isstringarray: ' int slen(#varname#) = 0;'}, ], 'callfortran':'#varname#,', - 'return':{isintent_out: ',capi_#varname#_tmp'}, + 'callfortranappend': {isstringarray: 'slen(#varname#),'}, + 'return': {isintent_out: ',capi_#varname#_as_array'}, 'need': 'len..', '_check': isarray }, { # intent(overwrite) array @@ -1057,36 +1095,48 @@ if (#varname#_cb.capi==Py_None) { 'keys_capi': {isoptional: ',&#varname#_capi'}, '_check': l_and(isarray, isintent_nothide) }, { - 'frompyobj': [' #setdims#;', - ' capi_#varname#_intent |= #intent#;', - {isintent_hide: - ' capi_#varname#_tmp = array_from_pyobj(#atype#,#varname#_Dims,#varname#_Rank,capi_#varname#_intent,Py_None);'}, - {isintent_nothide: - ' capi_#varname#_tmp = array_from_pyobj(#atype#,#varname#_Dims,#varname#_Rank,capi_#varname#_intent,#varname#_capi);'}, - """\ - if (capi_#varname#_tmp == NULL) { - PyObject *exc, *val, *tb; - PyErr_Fetch(&exc, &val, &tb); - PyErr_SetString(exc ? exc : #modulename#_error,\"failed in converting #nth# `#varname#\' of #pyname# to C/Fortran array\" ); - npy_PyErr_ChainExceptionsCause(exc, val, tb); + 'frompyobj': [ + ' #setdims#;', + ' capi_#varname#_intent |= #intent#;', + (' const char * capi_errmess = "#modulename#.#pyname#:' + ' failed to create array from the #nth# `#varname#`";'), + {isintent_hide: + ' capi_#varname#_as_array = ndarray_from_pyobj(' + ' #atype#,#elsize#,#varname#_Dims,#varname#_Rank,' + ' capi_#varname#_intent,Py_None,capi_errmess);'}, + {isintent_nothide: + ' capi_#varname#_as_array = ndarray_from_pyobj(' + ' #atype#,#elsize#,#varname#_Dims,#varname#_Rank,' + ' capi_#varname#_intent,#varname#_capi,capi_errmess);'}, + """\ + if (capi_#varname#_as_array == NULL) { + PyObject* capi_err = PyErr_Occurred(); + if (capi_err == NULL) { + capi_err = #modulename#_error; + PyErr_SetString(capi_err, capi_errmess); + } } else { - #varname# = (#ctype# *)(PyArray_DATA(capi_#varname#_tmp)); + #varname# = (#ctype# *)(PyArray_DATA(capi_#varname#_as_array)); """, - {hasinitvalue: [ - {isintent_nothide: - ' if (#varname#_capi == Py_None) {'}, - {isintent_hide: ' {'}, - {iscomplexarray: ' #ctype# capi_c;'}, - """\ + {isstringarray: + ' slen(#varname#) = f2py_itemsize(#varname#);'}, + {hasinitvalue: [ + {isintent_nothide: + ' if (#varname#_capi == Py_None) {'}, + {isintent_hide: ' {'}, + {iscomplexarray: ' #ctype# capi_c;'}, + """\ int *_i,capi_i=0; CFUNCSMESS(\"#name#: Initializing #varname#=#init#\\n\"); - if (initforcomb(PyArray_DIMS(capi_#varname#_tmp),PyArray_NDIM(capi_#varname#_tmp),1)) { + if (initforcomb(PyArray_DIMS(capi_#varname#_as_array), + PyArray_NDIM(capi_#varname#_as_array),1)) { while ((_i = nextforcomb())) #varname#[capi_i++] = #init#; /* fortran way */ } else { PyObject *exc, *val, *tb; PyErr_Fetch(&exc, &val, &tb); - PyErr_SetString(exc ? exc : #modulename#_error,\"Initialization of #nth# #varname# failed (initforcomb).\"); + PyErr_SetString(exc ? exc : #modulename#_error, + \"Initialization of #nth# #varname# failed (initforcomb).\"); npy_PyErr_ChainExceptionsCause(exc, val, tb); f2py_success = 0; } @@ -1094,12 +1144,13 @@ if (#varname#_cb.capi==Py_None) { if (f2py_success) {"""]}, ], 'cleanupfrompyobj': [ # note that this list will be reversed - ' } /*if (capi_#varname#_tmp == NULL) ... else of #varname#*/', + ' } ' + '/* if (capi_#varname#_as_array == NULL) ... else of #varname# */', {l_not(l_or(isintent_out, isintent_hide)): """\ - if((PyObject *)capi_#varname#_tmp!=#varname#_capi) { - Py_XDECREF(capi_#varname#_tmp); }"""}, + if((PyObject *)capi_#varname#_as_array!=#varname#_capi) { + Py_XDECREF(capi_#varname#_as_array); }"""}, {l_and(isintent_hide, l_not(isintent_out)) - : """ Py_XDECREF(capi_#varname#_tmp);"""}, + : """ Py_XDECREF(capi_#varname#_as_array);"""}, {hasinitvalue: ' } /*if (f2py_success) of #varname# init*/'}, ], '_check': isarray, @@ -1136,6 +1187,16 @@ if (#varname#_cb.capi==Py_None) { '_check': iscomplexarray, '_depend': '' }, + # Character + { + 'need': 'string', + '_check': ischaracter, + }, + # Character array + { + 'need': 'string', + '_check': ischaracterarray, + }, # Stringarray { 'callfortranappend': {isarrayofstrings: 'flen(#varname#),'}, diff --git a/numpy/f2py/src/fortranobject.c b/numpy/f2py/src/fortranobject.c index c96378170..8282655e5 100644 --- a/numpy/f2py/src/fortranobject.c +++ b/numpy/f2py/src/fortranobject.c @@ -5,6 +5,7 @@ extern "C" { #endif +#include <stdarg.h> #include <stdlib.h> #include <string.h> @@ -101,6 +102,20 @@ F2PyGetThreadLocalCallbackPtr(char *key) return prev; } +static PyArray_Descr * +get_descr_from_type_and_elsize(const int type_num, const int elsize) { + PyArray_Descr * descr = PyArray_DescrFromType(type_num); + if (type_num == NPY_STRING) { + // PyArray_DescrFromType returns descr with elsize = 0. + PyArray_DESCR_REPLACE(descr); + if (descr == NULL) { + return NULL; + } + descr->elsize = elsize; + } + return descr; +} + /************************* FortranObject *******************************/ typedef PyObject *(*fortranfunc)(PyObject *, PyObject *, PyObject *, void *); @@ -141,18 +156,17 @@ PyFortranObject_New(FortranDataDef *defs, f2py_void_func init) } else if ((fp->defs[i].data) != NULL) { /* Is Fortran variable or array (not allocatable) */ - if (fp->defs[i].type == NPY_STRING) { - npy_intp n = fp->defs[i].rank - 1; - v = PyArray_New(&PyArray_Type, n, fp->defs[i].dims.d, - NPY_STRING, NULL, fp->defs[i].data, - fp->defs[i].dims.d[n], NPY_ARRAY_FARRAY, NULL); - } - else { - v = PyArray_New(&PyArray_Type, fp->defs[i].rank, - fp->defs[i].dims.d, fp->defs[i].type, NULL, - fp->defs[i].data, 0, NPY_ARRAY_FARRAY, NULL); + PyArray_Descr * + descr = get_descr_from_type_and_elsize(fp->defs[i].type, + fp->defs[i].elsize); + if (descr == NULL) { + goto fail; } + v = PyArray_NewFromDescr(&PyArray_Type, descr, fp->defs[i].rank, + fp->defs[i].dims.d, NULL, fp->defs[i].data, + NPY_ARRAY_FARRAY, NULL); if (v == NULL) { + Py_DECREF(descr); goto fail; } PyDict_SetItemString(fp->dict, fp->defs[i].name, v); @@ -178,6 +192,13 @@ PyFortranObject_NewAsAttr(FortranDataDef *defs) } fp->len = 1; fp->defs = defs; + if (defs->rank == -1) { + PyDict_SetItemString(fp->dict, "__name__", PyUnicode_FromFormat("function %s", defs->name)); + } else if (defs->rank == 0) { + PyDict_SetItemString(fp->dict, "__name__", PyUnicode_FromFormat("scalar %s", defs->name)); + } else { + PyDict_SetItemString(fp->dict, "__name__", PyUnicode_FromFormat("array %s", defs->name)); + } return (PyObject *)fp; } @@ -697,9 +718,10 @@ f2py_report_on_array_copy_fromany(void) * $Id: fortranobject.c,v 1.52 2005/07/11 07:44:20 pearu Exp $ */ -static int -check_and_fix_dimensions(const PyArrayObject *arr, const int rank, - npy_intp *dims); +static int check_and_fix_dimensions(const PyArrayObject* arr, + const int rank, + npy_intp *dims, + const char *errmess); static int find_first_negative_dimension(const int rank, const npy_intp *dims) @@ -762,80 +784,176 @@ swap_arrays(PyArrayObject *obj1, PyArrayObject *obj2) return 0; } -#define ARRAY_ISCOMPATIBLE(arr, type_num) \ - ((PyArray_ISINTEGER(arr) && PyTypeNum_ISINTEGER(type_num)) || \ - (PyArray_ISFLOAT(arr) && PyTypeNum_ISFLOAT(type_num)) || \ - (PyArray_ISCOMPLEX(arr) && PyTypeNum_ISCOMPLEX(type_num)) || \ - (PyArray_ISBOOL(arr) && PyTypeNum_ISBOOL(type_num))) +#define ARRAY_ISCOMPATIBLE(arr,type_num) \ + ((PyArray_ISINTEGER(arr) && PyTypeNum_ISINTEGER(type_num)) || \ + (PyArray_ISFLOAT(arr) && PyTypeNum_ISFLOAT(type_num)) || \ + (PyArray_ISCOMPLEX(arr) && PyTypeNum_ISCOMPLEX(type_num)) || \ + (PyArray_ISBOOL(arr) && PyTypeNum_ISBOOL(type_num)) || \ + (PyArray_ISSTRING(arr) && PyTypeNum_ISSTRING(type_num))) + +static int +get_elsize(PyObject *obj) { + /* + get_elsize determines array itemsize from a Python object. Returns + elsize if succesful, -1 otherwise. + + Supported types of the input are: numpy.ndarray, bytes, str, tuple, + list. + */ + + if (PyArray_Check(obj)) { + return PyArray_DESCR((PyArrayObject *)obj)->elsize; + } else if (PyBytes_Check(obj)) { + return PyBytes_GET_SIZE(obj); + } else if (PyUnicode_Check(obj)) { + return PyUnicode_GET_LENGTH(obj); + } else if (PySequence_Check(obj)) { + PyObject* fast = PySequence_Fast(obj, "f2py:fortranobject.c:get_elsize"); + if (fast != NULL) { + Py_ssize_t i, n = PySequence_Fast_GET_SIZE(fast); + int sz, elsize = 0; + for (i=0; i<n; i++) { + sz = get_elsize(PySequence_Fast_GET_ITEM(fast, i) /* borrowed */); + if (sz > elsize) { + elsize = sz; + } + } + Py_DECREF(fast); + return elsize; + } + } + return -1; +} extern PyArrayObject * -array_from_pyobj(const int type_num, npy_intp *dims, const int rank, - const int intent, PyObject *obj) -{ +ndarray_from_pyobj(const int type_num, + const int elsize_, + npy_intp *dims, + const int rank, + const int intent, + PyObject *obj, + const char *errmess) { /* + * Return an array with given element type and shape from a Python + * object while taking into account the usage intent of the array. + * + * - element type is defined by type_num and elsize + * - shape is defined by dims and rank + * + * ndarray_from_pyobj is used to convert Python object arguments + * to numpy ndarrays with given type and shape that data is passed + * to interfaced Fortran or C functions. + * + * errmess (if not NULL), contains a prefix of an error message + * for an exception to be triggered within this function. + * + * Negative elsize value means that elsize is to be determined + * from the Python object in runtime. + * + * Note on strings + * --------------- + * + * String type (type_num == NPY_STRING) does not have fixed + * element size and, by default, the type object sets it to + * 0. Therefore, for string types, one has to use elsize + * argument. For other types, elsize value is ignored. + * + * NumPy defines the type of a fixed-width string as + * dtype('S<width>'). In addition, there is also dtype('c'), that + * appears as dtype('S1') (these have the same type_num value), + * but is actually different (.char attribute is either 'S' or + * 'c', respecitely). + * + * In Fortran, character arrays and strings are different + * concepts. The relation between Fortran types, NumPy dtypes, + * and type_num-elsize pairs, is defined as follows: + * + * character*5 foo | dtype('S5') | elsize=5, shape=() + * character(5) foo | dtype('S1') | elsize=1, shape=(5) + * character*5 foo(n) | dtype('S5') | elsize=5, shape=(n,) + * character(5) foo(n) | dtype('S1') | elsize=1, shape=(5, n) + * character*(*) foo | dtype('S') | elsize=-1, shape=() + * * Note about reference counting - * ----------------------------- + * ----------------------------- + * * If the caller returns the array to Python, it must be done with - * Py_BuildValue("N",arr). - * Otherwise, if obj!=arr then the caller must call Py_DECREF(arr). + * Py_BuildValue("N",arr). Otherwise, if obj!=arr then the caller + * must call Py_DECREF(arr). * * Note on intent(cache,out,..) - * --------------------- + * ---------------------------- * Don't expect correct data when returning intent(cache) array. * */ - char mess[200]; + char mess[F2PY_MESSAGE_BUFFER_SIZE]; PyArrayObject *arr = NULL; - PyArray_Descr *descr; - char typechar; - int elsize; - - if ((intent & F2PY_INTENT_HIDE) || - ((intent & F2PY_INTENT_CACHE) && (obj == Py_None)) || - ((intent & F2PY_OPTIONAL) && (obj == Py_None))) { + int elsize = (elsize_ < 0 ? get_elsize(obj) : elsize_); + if (elsize < 0) { + if (errmess != NULL) { + strcpy(mess, errmess); + } + sprintf(mess + strlen(mess), + " -- failed to determine element size from %s", + Py_TYPE(obj)->tp_name); + PyErr_SetString(PyExc_SystemError, mess); + return NULL; + } + PyArray_Descr * descr = get_descr_from_type_and_elsize(type_num, elsize); // new reference + if (descr == NULL) { + return NULL; + } + elsize = descr->elsize; + if ((intent & F2PY_INTENT_HIDE) + || ((intent & F2PY_INTENT_CACHE) && (obj == Py_None)) + || ((intent & F2PY_OPTIONAL) && (obj == Py_None)) + ) { /* intent(cache), optional, intent(hide) */ - int i = find_first_negative_dimension(rank, dims); - if (i >= 0) { - PyErr_Format(PyExc_ValueError, - "failed to create intent(cache|hide)|optional array" - " -- must have defined dimensions, but dims[%d] = %" - NPY_INTP_FMT, i, dims[i]); + int ineg = find_first_negative_dimension(rank, dims); + if (ineg >= 0) { + int i; + strcpy(mess, "failed to create intent(cache|hide)|optional array" + "-- must have defined dimensions but got ("); + for(i = 0; i < rank; ++i) + sprintf(mess + strlen(mess), "%" NPY_INTP_FMT ",", dims[i]); + strcat(mess, ")"); + PyErr_SetString(PyExc_ValueError, mess); + Py_DECREF(descr); return NULL; } - arr = (PyArrayObject *)PyArray_New(&PyArray_Type, rank, dims, type_num, - NULL, NULL, 1, - !(intent & F2PY_INTENT_C), NULL); - if (arr == NULL) - return NULL; - if (!(intent & F2PY_INTENT_CACHE)) - PyArray_FILLWBYTE(arr, 0); + arr = (PyArrayObject *) \ + PyArray_NewFromDescr(&PyArray_Type, descr, rank, dims, + NULL, NULL, !(intent & F2PY_INTENT_C), NULL); + if (arr == NULL) { + Py_DECREF(descr); + return NULL; + } + if (PyArray_ITEMSIZE(arr) != elsize) { + strcpy(mess, "failed to create intent(cache|hide)|optional array"); + sprintf(mess+strlen(mess)," -- expected elsize=%d got %" NPY_INTP_FMT, elsize, (npy_intp)PyArray_ITEMSIZE(arr)); + PyErr_SetString(PyExc_ValueError,mess); + Py_DECREF(arr); + return NULL; + } + if (!(intent & F2PY_INTENT_CACHE)) { + PyArray_FILLWBYTE(arr, 0); + } return arr; } - descr = PyArray_DescrFromType(type_num); - /* compatibility with NPY_CHAR */ - if (type_num == NPY_STRING) { - PyArray_DESCR_REPLACE(descr); - if (descr == NULL) { - return NULL; - } - descr->elsize = 1; - descr->type = NPY_CHARLTR; - } - elsize = descr->elsize; - typechar = descr->type; - Py_DECREF(descr); if (PyArray_Check(obj)) { arr = (PyArrayObject *)obj; - if (intent & F2PY_INTENT_CACHE) { /* intent(cache) */ - if (PyArray_ISONESEGMENT(arr) && PyArray_ITEMSIZE(arr) >= elsize) { - if (check_and_fix_dimensions(arr, rank, dims)) { - return NULL; + if (PyArray_ISONESEGMENT(arr) + && PyArray_ITEMSIZE(arr) >= elsize) { + if (check_and_fix_dimensions(arr, rank, dims, errmess)) { + Py_DECREF(descr); + return NULL; } if (intent & F2PY_INTENT_OUT) - Py_INCREF(arr); + Py_INCREF(arr); + Py_DECREF(descr); return arr; } strcpy(mess, "failed to initialize intent(cache) array"); @@ -847,14 +965,16 @@ array_from_pyobj(const int type_num, npy_intp *dims, const int rank, "%" NPY_INTP_FMT, elsize, (npy_intp)PyArray_ITEMSIZE(arr)); PyErr_SetString(PyExc_ValueError, mess); + Py_DECREF(descr); return NULL; } /* here we have always intent(in) or intent(inout) or intent(inplace) */ - if (check_and_fix_dimensions(arr, rank, dims)) { - return NULL; + if (check_and_fix_dimensions(arr, rank, dims, errmess)) { + Py_DECREF(descr); + return NULL; } /* printf("intent alignment=%d\n", F2PY_GET_ALIGNMENT(intent)); @@ -863,16 +983,18 @@ array_from_pyobj(const int type_num, npy_intp *dims, const int rank, for (i=1;i<=16;i++) printf("i=%d isaligned=%d\n", i, ARRAY_ISALIGNED(arr, i)); */ - if ((!(intent & F2PY_INTENT_COPY)) && + if ((! (intent & F2PY_INTENT_COPY)) && PyArray_ITEMSIZE(arr) == elsize && - ARRAY_ISCOMPATIBLE(arr, type_num) && + ARRAY_ISCOMPATIBLE(arr,type_num) && F2PY_CHECK_ALIGNMENT(arr, intent)) { - if ((intent & F2PY_INTENT_C) ? PyArray_ISCARRAY_RO(arr) - : PyArray_ISFARRAY_RO(arr)) { + if ((intent & F2PY_INTENT_INOUT || intent & F2PY_INTENT_INPLACE) + ? ((intent & F2PY_INTENT_C) ? PyArray_ISCARRAY(arr) : PyArray_ISFARRAY(arr)) + : ((intent & F2PY_INTENT_C) ? PyArray_ISCARRAY_RO(arr) : PyArray_ISFARRAY_RO(arr))) { if ((intent & F2PY_INTENT_OUT)) { Py_INCREF(arr); } /* Returning input array */ + Py_DECREF(descr); return arr; } } @@ -887,42 +1009,48 @@ array_from_pyobj(const int type_num, npy_intp *dims, const int rank, if (PyArray_ITEMSIZE(arr) != elsize) sprintf(mess + strlen(mess), " -- expected elsize=%d but got %" NPY_INTP_FMT, - elsize, (npy_intp)PyArray_ITEMSIZE(arr)); - if (!(ARRAY_ISCOMPATIBLE(arr, type_num))) + elsize, + (npy_intp)PyArray_ITEMSIZE(arr) + ); + if (!(ARRAY_ISCOMPATIBLE(arr, type_num))) { sprintf(mess + strlen(mess), " -- input '%c' not compatible to '%c'", - PyArray_DESCR(arr)->type, typechar); + PyArray_DESCR(arr)->type, descr->type); + } if (!(F2PY_CHECK_ALIGNMENT(arr, intent))) sprintf(mess + strlen(mess), " -- input not %d-aligned", F2PY_GET_ALIGNMENT(intent)); PyErr_SetString(PyExc_ValueError, mess); + Py_DECREF(descr); return NULL; } /* here we have always intent(in) or intent(inplace) */ { - PyArrayObject *retarr; - retarr = (PyArrayObject *)PyArray_New( - &PyArray_Type, PyArray_NDIM(arr), PyArray_DIMS(arr), - type_num, NULL, NULL, 1, !(intent & F2PY_INTENT_C), NULL); - if (retarr == NULL) - return NULL; - F2PY_REPORT_ON_ARRAY_COPY_FROMARR; - if (PyArray_CopyInto(retarr, arr)) { - Py_DECREF(retarr); - return NULL; - } - if (intent & F2PY_INTENT_INPLACE) { - if (swap_arrays(arr, retarr)) - return NULL; /* XXX: set exception */ - Py_XDECREF(retarr); - if (intent & F2PY_INTENT_OUT) - Py_INCREF(arr); - } - else { - arr = retarr; + PyArrayObject * retarr = (PyArrayObject *) \ + PyArray_NewFromDescr(&PyArray_Type, descr, PyArray_NDIM(arr), PyArray_DIMS(arr), + NULL, NULL, !(intent & F2PY_INTENT_C), NULL); + if (retarr==NULL) { + Py_DECREF(descr); + return NULL; + } + F2PY_REPORT_ON_ARRAY_COPY_FROMARR; + if (PyArray_CopyInto(retarr, arr)) { + Py_DECREF(retarr); + return NULL; + } + if (intent & F2PY_INTENT_INPLACE) { + if (swap_arrays(arr,retarr)) { + Py_DECREF(retarr); + return NULL; /* XXX: set exception */ } + Py_XDECREF(retarr); + if (intent & F2PY_INTENT_OUT) + Py_INCREF(arr); + } else { + arr = retarr; + } } return arr; } @@ -933,20 +1061,11 @@ array_from_pyobj(const int type_num, npy_intp *dims, const int rank, "failed to initialize intent(inout|inplace|cache) " "array, input '%s' object is not an array", Py_TYPE(obj)->tp_name); + Py_DECREF(descr); return NULL; } { - PyArray_Descr *descr = PyArray_DescrFromType(type_num); - /* compatibility with NPY_CHAR */ - if (type_num == NPY_STRING) { - PyArray_DESCR_REPLACE(descr); - if (descr == NULL) { - return NULL; - } - descr->elsize = 1; - descr->type = NPY_CHARLTR; - } F2PY_REPORT_ON_ARRAY_COPY_FROMANY; arr = (PyArrayObject *)PyArray_FromAny( obj, descr, 0, 0, @@ -954,22 +1073,54 @@ array_from_pyobj(const int type_num, npy_intp *dims, const int rank, : NPY_ARRAY_FARRAY) | NPY_ARRAY_FORCECAST, NULL); - if (arr == NULL) - return NULL; - if (check_and_fix_dimensions(arr, rank, dims)) { - return NULL; + // Warning: in the case of NPY_STRING, PyArray_FromAny may + // reset descr->elsize, e.g. dtype('S0') becomes dtype('S1'). + if (arr == NULL) { + Py_DECREF(descr); + return NULL; + } + if (type_num != NPY_STRING && PyArray_ITEMSIZE(arr) != elsize) { + // This is internal sanity tests: elsize has been set to + // descr->elsize in the beginning of this function. + strcpy(mess, "failed to initialize intent(in) array"); + sprintf(mess + strlen(mess), + " -- expected elsize=%d got %" NPY_INTP_FMT, elsize, + (npy_intp)PyArray_ITEMSIZE(arr)); + PyErr_SetString(PyExc_ValueError, mess); + Py_DECREF(arr); + return NULL; + } + if (check_and_fix_dimensions(arr, rank, dims, errmess)) { + Py_DECREF(arr); + return NULL; } return arr; } } +extern PyArrayObject * +array_from_pyobj(const int type_num, + npy_intp *dims, + const int rank, + const int intent, + PyObject *obj) { + /* + Same as ndarray_from_pyobj but with elsize determined from type, + if possible. Provided for backward compatibility. + */ + PyArray_Descr* descr = PyArray_DescrFromType(type_num); + int elsize = descr->elsize; + Py_DECREF(descr); + return ndarray_from_pyobj(type_num, elsize, dims, rank, intent, obj, NULL); +} + /*****************************************/ /* Helper functions for array_from_pyobj */ /*****************************************/ static int -check_and_fix_dimensions(const PyArrayObject *arr, const int rank, - npy_intp *dims) +check_and_fix_dimensions(const PyArrayObject* arr, const int rank, + npy_intp *dims, const char *errmess) { /* * This function fills in blanks (that are -1's) in dims list using @@ -981,6 +1132,7 @@ check_and_fix_dimensions(const PyArrayObject *arr, const int rank, * If an error condition is detected, an exception is set and 1 is * returned. */ + char mess[F2PY_MESSAGE_BUFFER_SIZE]; const npy_intp arr_size = (PyArray_NDIM(arr)) ? PyArray_Size((PyObject *)arr) : 1; #ifdef DEBUG_COPY_ND_ARRAY @@ -1046,11 +1198,14 @@ check_and_fix_dimensions(const PyArrayObject *arr, const int rank, d = PyArray_DIM(arr, i); if (dims[i] >= 0) { if (d > 1 && d != dims[i]) { - PyErr_Format( - PyExc_ValueError, - "%d-th dimension must be fixed to %" NPY_INTP_FMT - " but got %" NPY_INTP_FMT "\n", + if (errmess != NULL) { + strcpy(mess, errmess); + } + sprintf(mess + strlen(mess), + " -- %d-th dimension must be fixed to %" + NPY_INTP_FMT " but got %" NPY_INTP_FMT, i, dims[i], d); + PyErr_SetString(PyExc_ValueError, mess); return 1; } if (!dims[i]) @@ -1093,11 +1248,15 @@ check_and_fix_dimensions(const PyArrayObject *arr, const int rank, d = PyArray_DIM(arr, j++); if (dims[i] >= 0) { if (d > 1 && d != dims[i]) { - PyErr_Format( - PyExc_ValueError, - "%d-th dimension must be fixed to %" NPY_INTP_FMT - " but got %" NPY_INTP_FMT " (real index=%d)\n", - i, dims[i], d, j - 1); + if (errmess != NULL) { + strcpy(mess, errmess); + } + sprintf(mess + strlen(mess), + " -- %d-th dimension must be fixed to %" + NPY_INTP_FMT " but got %" NPY_INTP_FMT + " (real index=%d)\n", + i, dims[i], d, j-1); + PyErr_SetString(PyExc_ValueError, mess); return 1; } if (!dims[i]) @@ -1161,6 +1320,72 @@ copy_ND_array(const PyArrayObject *arr, PyArrayObject *out) return PyArray_CopyInto(out, (PyArrayObject *)arr); } +/********************* Various utility functions ***********************/ + +extern int +f2py_describe(PyObject *obj, char *buf) { + /* + Write the description of a Python object to buf. The caller must + provide buffer with size sufficient to write the description. + + Return 1 on success. + */ + char localbuf[F2PY_MESSAGE_BUFFER_SIZE]; + if (PyBytes_Check(obj)) { + sprintf(localbuf, "%d-%s", (npy_int)PyBytes_GET_SIZE(obj), Py_TYPE(obj)->tp_name); + } else if (PyUnicode_Check(obj)) { + sprintf(localbuf, "%d-%s", (npy_int)PyUnicode_GET_LENGTH(obj), Py_TYPE(obj)->tp_name); + } else if (PyArray_CheckScalar(obj)) { + PyArrayObject* arr = (PyArrayObject*)obj; + sprintf(localbuf, "%c%" NPY_INTP_FMT "-%s-scalar", PyArray_DESCR(arr)->kind, PyArray_ITEMSIZE(arr), Py_TYPE(obj)->tp_name); + } else if (PyArray_Check(obj)) { + int i; + PyArrayObject* arr = (PyArrayObject*)obj; + strcpy(localbuf, "("); + for (i=0; i<PyArray_NDIM(arr); i++) { + if (i) { + strcat(localbuf, " "); + } + sprintf(localbuf + strlen(localbuf), "%" NPY_INTP_FMT ",", PyArray_DIM(arr, i)); + } + sprintf(localbuf + strlen(localbuf), ")-%c%" NPY_INTP_FMT "-%s", PyArray_DESCR(arr)->kind, PyArray_ITEMSIZE(arr), Py_TYPE(obj)->tp_name); + } else if (PySequence_Check(obj)) { + sprintf(localbuf, "%d-%s", (npy_int)PySequence_Length(obj), Py_TYPE(obj)->tp_name); + } else { + sprintf(localbuf, "%s instance", Py_TYPE(obj)->tp_name); + } + // TODO: detect the size of buf and make sure that size(buf) >= size(localbuf). + strcpy(buf, localbuf); + return 1; +} + +extern npy_intp +f2py_size_impl(PyArrayObject* var, ...) +{ + npy_intp sz = 0; + npy_intp dim; + npy_intp rank; + va_list argp; + va_start(argp, var); + dim = va_arg(argp, npy_int); + if (dim==-1) + { + sz = PyArray_SIZE(var); + } + else + { + rank = PyArray_NDIM(var); + if (dim>=1 && dim<=rank) + sz = PyArray_DIM(var, dim-1); + else + fprintf(stderr, "f2py_size: 2nd argument value=%" NPY_INTP_FMT + " fails to satisfy 1<=value<=%" NPY_INTP_FMT + ". Result will be 0.\n", dim, rank); + } + va_end(argp); + return sz; +} + /*********************************************/ /* Compatibility functions for Python >= 3.0 */ /*********************************************/ diff --git a/numpy/f2py/src/fortranobject.h b/numpy/f2py/src/fortranobject.h index a1e9fdbdf..376b83dad 100644 --- a/numpy/f2py/src/fortranobject.h +++ b/numpy/f2py/src/fortranobject.h @@ -6,6 +6,7 @@ extern "C" { #include <Python.h> +#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION #ifdef FORTRANOBJECT_C #define NO_IMPORT_ARRAY #endif @@ -44,6 +45,7 @@ Author: Pearu Peterson <pearu@cens.ioc.ee> */ #define F2PY_MAX_DIMS 40 +#define F2PY_MESSAGE_BUFFER_SIZE 300 // Increase on "stack smashing detected" typedef void (*f2py_set_data_func)(char *, npy_intp *); typedef void (*f2py_void_func)(void); @@ -61,6 +63,7 @@ typedef struct { npy_intp d[F2PY_MAX_DIMS]; } dims; /* dimensions of the array, || not used */ int type; /* PyArray_<type> || not used */ + int elsize; /* Element size || not used */ char *data; /* pointer to array || Fortran routine */ f2py_init_func func; /* initialization function for allocatable arrays: @@ -125,6 +128,14 @@ F2PyGetThreadLocalCallbackPtr(char *key); : (F2PY_ALIGN8(intent) ? 8 : (F2PY_ALIGN16(intent) ? 16 : 1))) #define F2PY_CHECK_ALIGNMENT(arr, intent) \ ARRAY_ISALIGNED(arr, F2PY_GET_ALIGNMENT(intent)) +#define F2PY_ARRAY_IS_CHARACTER_COMPATIBLE(arr) ((PyArray_DESCR(arr)->type_num == NPY_STRING && PyArray_DESCR(arr)->elsize >= 1) \ + || PyArray_DESCR(arr)->type_num == NPY_UINT8) +#define F2PY_IS_UNICODE_ARRAY(arr) (PyArray_DESCR(arr)->type_num == NPY_UNICODE) + +extern PyArrayObject * +ndarray_from_pyobj(const int type_num, const int elsize_, npy_intp *dims, + const int rank, const int intent, PyObject *obj, + const char *errmess); extern PyArrayObject * array_from_pyobj(const int type_num, npy_intp *dims, const int rank, @@ -137,6 +148,23 @@ extern void dump_attrs(const PyArrayObject *arr); #endif + extern int f2py_describe(PyObject *obj, char *buf); + + /* Utility CPP macros and functions that can be used in signature file + expressions. See signature-file.rst for documentation. + */ + +#define f2py_itemsize(var) (PyArray_DESCR((capi_ ## var ## _as_array))->elsize) +#define f2py_size(var, ...) f2py_size_impl((PyArrayObject *)(capi_ ## var ## _as_array), ## __VA_ARGS__, -1) +#define f2py_rank(var) var ## _Rank +#define f2py_shape(var,dim) var ## _Dims[dim] +#define f2py_len(var) f2py_shape(var,0) +#define f2py_fshape(var,dim) f2py_shape(var,rank(var)-dim-1) +#define f2py_flen(var) f2py_fshape(var,0) +#define f2py_slen(var) capi_ ## var ## _len + + extern npy_intp f2py_size_impl(PyArrayObject* var, ...); + #ifdef __cplusplus } #endif diff --git a/numpy/f2py/tests/src/array_from_pyobj/wrapmodule.c b/numpy/f2py/tests/src/array_from_pyobj/wrapmodule.c index c8ae7b9dc..9a8b4a752 100644 --- a/numpy/f2py/tests/src/array_from_pyobj/wrapmodule.c +++ b/numpy/f2py/tests/src/array_from_pyobj/wrapmodule.c @@ -33,6 +33,7 @@ static PyObject *f2py_rout_wrap_call(PyObject *capi_self, PyObject *capi_args) { PyObject * volatile capi_buildvalue = NULL; int type_num = 0; + int elsize = 0; npy_intp *dims = NULL; PyObject *dims_capi = Py_None; int rank = 0; @@ -41,8 +42,8 @@ static PyObject *f2py_rout_wrap_call(PyObject *capi_self, PyObject *arr_capi = Py_None; int i; - if (!PyArg_ParseTuple(capi_args,"iOiO|:wrap.call",\ - &type_num,&dims_capi,&intent,&arr_capi)) + if (!PyArg_ParseTuple(capi_args,"iiOiO|:wrap.call",\ + &type_num,&elsize,&dims_capi,&intent,&arr_capi)) return NULL; rank = PySequence_Length(dims_capi); dims = malloc(rank*sizeof(npy_intp)); @@ -58,7 +59,7 @@ static PyObject *f2py_rout_wrap_call(PyObject *capi_self, goto fail; } } - capi_arr_tmp = array_from_pyobj(type_num,dims,rank,intent|F2PY_INTENT_OUT,arr_capi); + capi_arr_tmp = ndarray_from_pyobj(type_num,elsize,dims,rank,intent|F2PY_INTENT_OUT,arr_capi,"wrap.call failed"); if (capi_arr_tmp == NULL) { free(dims); return NULL; diff --git a/numpy/f2py/tests/test_array_from_pyobj.py b/numpy/f2py/tests/test_array_from_pyobj.py index 5a084bc3e..2b8c8defc 100644 --- a/numpy/f2py/tests/test_array_from_pyobj.py +++ b/numpy/f2py/tests/test_array_from_pyobj.py @@ -6,11 +6,18 @@ import pytest import numpy as np -from numpy.core.multiarray import typeinfo +from numpy.testing import assert_, assert_equal +from numpy.core.multiarray import typeinfo as _typeinfo from . import util wrap = None +# Extend core typeinfo with CHARACTER to test dtype('c') +_ti = _typeinfo['STRING'] +typeinfo = dict( + CHARACTER=type(_ti)(('c', _ti.num, 8, _ti.alignment, _ti.type)), + **_typeinfo) + def setup_module(): """ @@ -56,6 +63,7 @@ def flags2names(flags): "NOTSWAPPED", "WRITEABLE", "WRITEBACKIFCOPY", + "UPDATEIFCOPY", "BEHAVED", "BEHAVED_RO", "CARRAY", @@ -116,6 +124,9 @@ _type_names = [ "FLOAT", "DOUBLE", "CFLOAT", + "STRING1", + "STRING5", + "CHARACTER", ] _cast_dict = {"BOOL": ["BOOL"]} @@ -139,6 +150,10 @@ _cast_dict["DOUBLE"] = _cast_dict["INT"] + ["UINT", "FLOAT", "DOUBLE"] _cast_dict["CFLOAT"] = _cast_dict["FLOAT"] + ["CFLOAT"] +_cast_dict['STRING1'] = ['STRING1'] +_cast_dict['STRING5'] = ['STRING5'] +_cast_dict['CHARACTER'] = ['CHARACTER'] + # 32 bit system malloc typically does not provide the alignment required by # 16 byte long double types this means the inout intent cannot be satisfied # and several tests fail as the alignment flag can be randomly true or fals @@ -184,14 +199,33 @@ class Type: def _init(self, name): self.NAME = name.upper() - info = typeinfo[self.NAME] - self.type_num = getattr(wrap, "NPY_" + self.NAME) + + if self.NAME == 'CHARACTER': + info = typeinfo[self.NAME] + self.type_num = getattr(wrap, 'NPY_STRING') + self.elsize = 1 + self.dtype = np.dtype('c') + elif self.NAME.startswith('STRING'): + info = typeinfo[self.NAME[:6]] + self.type_num = getattr(wrap, 'NPY_STRING') + self.elsize = int(self.NAME[6:] or 0) + self.dtype = np.dtype(f'S{self.elsize}') + else: + info = typeinfo[self.NAME] + self.type_num = getattr(wrap, 'NPY_' + self.NAME) + self.elsize = info.bits // 8 + self.dtype = np.dtype(info.type) + assert self.type_num == info.num - self.dtype = np.dtype(info.type) self.type = info.type - self.elsize = info.bits / 8 self.dtypechar = info.char + def __repr__(self): + return (f"Type({self.NAME})|type_num={self.type_num}," + f" dtype={self.dtype}," + f" type={self.type}, elsize={self.elsize}," + f" dtypechar={self.dtypechar}") + def cast_types(self): return [self.__class__(_m) for _m in _cast_dict[self.NAME]] @@ -226,6 +260,11 @@ class Type: class Array: + + def __repr__(self): + return (f'Array({self.type}, {self.dims}, {self.intent},' + f' {self.obj})|arr={self.arr}') + def __init__(self, typ, dims, intent, obj): self.type = typ self.dims = dims @@ -234,7 +273,9 @@ class Array: self.obj = obj # arr.dtypechar may be different from typ.dtypechar - self.arr = wrap.call(typ.type_num, dims, intent.flags, obj) + self.arr = wrap.call(typ.type_num, + typ.elsize, + dims, intent.flags, obj) assert isinstance(self.arr, np.ndarray) @@ -289,7 +330,9 @@ class Array: self.arr.tobytes(), self.pyarr.tobytes(), )) # strides - assert self.arr_attr[5][-2:] == self.pyarr_attr[5][-2:] # descr + assert self.arr_attr[5][-2:] == self.pyarr_attr[5][-2:], repr(( + self.arr_attr[5], self.pyarr_attr[5] + )) # descr assert self.arr_attr[6] == self.pyarr_attr[6], repr(( self.arr_attr[6], self.pyarr_attr[6], @@ -338,8 +381,6 @@ class TestIntent: class TestSharedMemory: - num2seq = [1, 2] - num23seq = [[1, 2, 3], [4, 5, 6]] @pytest.fixture(autouse=True, scope="class", params=_type_names) def setup_type(self, request): @@ -347,6 +388,21 @@ class TestSharedMemory: request.cls.array = lambda self, dims, intent, obj: Array( Type(request.param), dims, intent, obj) + @property + def num2seq(self): + if self.type.NAME.startswith('STRING'): + elsize = self.type.elsize + return ['1' * elsize, '2' * elsize] + return [1, 2] + + @property + def num23seq(self): + if self.type.NAME.startswith('STRING'): + elsize = self.type.elsize + return [['1' * elsize, '2' * elsize, '3' * elsize], + ['4' * elsize, '5' * elsize, '6' * elsize]] + return [[1, 2, 3], [4, 5, 6]] + def test_in_from_2seq(self): a = self.array([2], intent.in_, self.num2seq) assert not a.has_shared_memory() @@ -367,9 +423,9 @@ class TestSharedMemory: """Test if intent(in) array can be passed without copies""" seq = getattr(self, "num" + inp) obj = np.array(seq, dtype=self.type.dtype, order=order) - obj.setflags(write=(write == "w")) + obj.setflags(write=(write == 'w')) a = self.array(obj.shape, - ((order == "C" and intent.in_.c) or intent.in_), obj) + ((order == 'C' and intent.in_.c) or intent.in_), obj) assert a.has_shared_memory() def test_inout_2seq(self): @@ -496,6 +552,9 @@ class TestSharedMemory: def test_in_cache_from_2casttype_failure(self): for t in self.type.all_types(): + if t.NAME == 'STRING': + # string elsize is 0, so skipping the test + continue if t.elsize >= self.type.elsize: continue obj = np.array(self.num2seq, dtype=t.dtype) diff --git a/numpy/f2py/tests/test_callback.py b/numpy/f2py/tests/test_callback.py index 4e91430fd..335b78413 100644 --- a/numpy/f2py/tests/test_callback.py +++ b/numpy/f2py/tests/test_callback.py @@ -84,8 +84,8 @@ class TestF77Callback(util.F2PyTest): r = t(a.mth) assert r == 9 - @pytest.mark.skipif(sys.platform == "win32", - reason="Fails with MinGW64 Gfortran (Issue #9673)") + @pytest.mark.skipif(sys.platform == 'win32', + reason='Fails with MinGW64 Gfortran (Issue #9673)') def test_string_callback(self): def callback(code): if code == "r": @@ -97,24 +97,27 @@ class TestF77Callback(util.F2PyTest): r = f(callback) assert r == 0 - @pytest.mark.skipif(sys.platform == "win32", - reason="Fails with MinGW64 Gfortran (Issue #9673)") + @pytest.mark.skipif(sys.platform == 'win32', + reason='Fails with MinGW64 Gfortran (Issue #9673)') def test_string_callback_array(self): # See gh-10027 - cu = np.zeros((1, 8), "S1") + cu1 = np.zeros((1, ), "S8") + cu2 = np.zeros((1, 8), "c") + cu3 = np.array([""], "S8") def callback(cu, lencu): - if cu.shape != (lencu, 8): + if cu.shape != (lencu,): return 1 - if cu.dtype != "S1": + if cu.dtype != "S8": return 2 if not np.all(cu == b""): return 3 return 0 f = getattr(self.module, "string_callback_array") - res = f(callback, cu, len(cu)) - assert res == 0 + for cu in [cu1, cu2, cu3]: + res = f(callback, cu, len(cu)) + assert res == 0 def test_threadsafety(self): # Segfaults if the callback handling is not threadsafe @@ -223,6 +226,5 @@ class TestGH18335(util.F2PyTest): def foo(x): x[0] += 1 - y = np.array([1, 2, 3], dtype=np.int8) r = self.module.gh18335(foo) assert r == 123 + 1 diff --git a/numpy/f2py/tests/test_character.py b/numpy/f2py/tests/test_character.py new file mode 100644 index 000000000..b54b4d981 --- /dev/null +++ b/numpy/f2py/tests/test_character.py @@ -0,0 +1,570 @@ +import pytest +import textwrap +from numpy.testing import assert_array_equal, assert_equal, assert_raises +import numpy as np +from numpy.f2py.tests import util + + +class TestCharacterString(util.F2PyTest): + # options = ['--debug-capi', '--build-dir', '/tmp/test-build-f2py'] + suffix = '.f90' + fprefix = 'test_character_string' + length_list = ['1', '3', 'star'] + + code = '' + for length in length_list: + fsuffix = length + clength = dict(star='(*)').get(length, length) + + code += textwrap.dedent(f""" + + subroutine {fprefix}_input_{fsuffix}(c, o, n) + character*{clength}, intent(in) :: c + integer n + !f2py integer, depend(c), intent(hide) :: n = slen(c) + integer*1, dimension(n) :: o + !f2py intent(out) o + o = transfer(c, o) + end subroutine {fprefix}_input_{fsuffix} + + subroutine {fprefix}_output_{fsuffix}(c, o, n) + character*{clength}, intent(out) :: c + integer n + integer*1, dimension(n), intent(in) :: o + !f2py integer, depend(o), intent(hide) :: n = len(o) + c = transfer(o, c) + end subroutine {fprefix}_output_{fsuffix} + + subroutine {fprefix}_array_input_{fsuffix}(c, o, m, n) + integer m, i, n + character*{clength}, intent(in), dimension(m) :: c + !f2py integer, depend(c), intent(hide) :: m = len(c) + !f2py integer, depend(c), intent(hide) :: n = f2py_itemsize(c) + integer*1, dimension(m, n), intent(out) :: o + do i=1,m + o(i, :) = transfer(c(i), o(i, :)) + end do + end subroutine {fprefix}_array_input_{fsuffix} + + subroutine {fprefix}_array_output_{fsuffix}(c, o, m, n) + character*{clength}, intent(out), dimension(m) :: c + integer n + integer*1, dimension(m, n), intent(in) :: o + !f2py character(f2py_len=n) :: c + !f2py integer, depend(o), intent(hide) :: m = len(o) + !f2py integer, depend(o), intent(hide) :: n = shape(o, 1) + do i=1,m + c(i) = transfer(o(i, :), c(i)) + end do + end subroutine {fprefix}_array_output_{fsuffix} + + subroutine {fprefix}_2d_array_input_{fsuffix}(c, o, m1, m2, n) + integer m1, m2, i, j, n + character*{clength}, intent(in), dimension(m1, m2) :: c + !f2py integer, depend(c), intent(hide) :: m1 = len(c) + !f2py integer, depend(c), intent(hide) :: m2 = shape(c, 1) + !f2py integer, depend(c), intent(hide) :: n = f2py_itemsize(c) + integer*1, dimension(m1, m2, n), intent(out) :: o + do i=1,m1 + do j=1,m2 + o(i, j, :) = transfer(c(i, j), o(i, j, :)) + end do + end do + end subroutine {fprefix}_2d_array_input_{fsuffix} + """) + + @pytest.mark.parametrize("length", length_list) + def test_input(self, length): + fsuffix = {'(*)': 'star'}.get(length, length) + f = getattr(self.module, self.fprefix + '_input_' + fsuffix) + + a = {'1': 'a', '3': 'abc', 'star': 'abcde' * 3}[length] + + assert_array_equal(f(a), np.array(list(map(ord, a)), dtype='u1')) + + @pytest.mark.parametrize("length", length_list[:-1]) + def test_output(self, length): + fsuffix = length + f = getattr(self.module, self.fprefix + '_output_' + fsuffix) + + a = {'1': 'a', '3': 'abc'}[length] + + assert_array_equal(f(np.array(list(map(ord, a)), dtype='u1')), + a.encode()) + + @pytest.mark.parametrize("length", length_list) + def test_array_input(self, length): + fsuffix = length + f = getattr(self.module, self.fprefix + '_array_input_' + fsuffix) + + a = np.array([{'1': 'a', '3': 'abc', 'star': 'abcde' * 3}[length], + {'1': 'A', '3': 'ABC', 'star': 'ABCDE' * 3}[length], + ], dtype='S') + + expected = np.array([[c for c in s] for s in a], dtype='u1') + assert_array_equal(f(a), expected) + + @pytest.mark.parametrize("length", length_list) + def test_array_output(self, length): + fsuffix = length + f = getattr(self.module, self.fprefix + '_array_output_' + fsuffix) + + expected = np.array( + [{'1': 'a', '3': 'abc', 'star': 'abcde' * 3}[length], + {'1': 'A', '3': 'ABC', 'star': 'ABCDE' * 3}[length]], dtype='S') + + a = np.array([[c for c in s] for s in expected], dtype='u1') + assert_array_equal(f(a), expected) + + @pytest.mark.parametrize("length", length_list) + def test_2d_array_input(self, length): + fsuffix = length + f = getattr(self.module, self.fprefix + '_2d_array_input_' + fsuffix) + + a = np.array([[{'1': 'a', '3': 'abc', 'star': 'abcde' * 3}[length], + {'1': 'A', '3': 'ABC', 'star': 'ABCDE' * 3}[length]], + [{'1': 'f', '3': 'fgh', 'star': 'fghij' * 3}[length], + {'1': 'F', '3': 'FGH', 'star': 'FGHIJ' * 3}[length]]], + dtype='S') + expected = np.array([[[c for c in item] for item in row] for row in a], + dtype='u1', order='F') + assert_array_equal(f(a), expected) + + +class TestCharacter(util.F2PyTest): + # options = ['--debug-capi', '--build-dir', '/tmp/test-build-f2py'] + suffix = '.f90' + fprefix = 'test_character' + + code = textwrap.dedent(f""" + subroutine {fprefix}_input(c, o) + character, intent(in) :: c + integer*1 o + !f2py intent(out) o + o = transfer(c, o) + end subroutine {fprefix}_input + + subroutine {fprefix}_output(c, o) + character :: c + integer*1, intent(in) :: o + !f2py intent(out) c + c = transfer(o, c) + end subroutine {fprefix}_output + + subroutine {fprefix}_input_output(c, o) + character, intent(in) :: c + character o + !f2py intent(out) o + o = c + end subroutine {fprefix}_input_output + + subroutine {fprefix}_inout(c, n) + character :: c, n + !f2py intent(in) n + !f2py intent(inout) c + c = n + end subroutine {fprefix}_inout + + function {fprefix}_return(o) result (c) + character :: c + character, intent(in) :: o + c = transfer(o, c) + end function {fprefix}_return + + subroutine {fprefix}_array_input(c, o) + character, intent(in) :: c(3) + integer*1 o(3) + !f2py intent(out) o + integer i + do i=1,3 + o(i) = transfer(c(i), o(i)) + end do + end subroutine {fprefix}_array_input + + subroutine {fprefix}_2d_array_input(c, o) + character, intent(in) :: c(2, 3) + integer*1 o(2, 3) + !f2py intent(out) o + integer i, j + do i=1,2 + do j=1,3 + o(i, j) = transfer(c(i, j), o(i, j)) + end do + end do + end subroutine {fprefix}_2d_array_input + + subroutine {fprefix}_array_output(c, o) + character :: c(3) + integer*1, intent(in) :: o(3) + !f2py intent(out) c + do i=1,3 + c(i) = transfer(o(i), c(i)) + end do + end subroutine {fprefix}_array_output + + subroutine {fprefix}_array_inout(c, n) + character :: c(3), n(3) + !f2py intent(in) n(3) + !f2py intent(inout) c(3) + do i=1,3 + c(i) = n(i) + end do + end subroutine {fprefix}_array_inout + + subroutine {fprefix}_2d_array_inout(c, n) + character :: c(2, 3), n(2, 3) + !f2py intent(in) n(2, 3) + !f2py intent(inout) c(2. 3) + integer i, j + do i=1,2 + do j=1,3 + c(i, j) = n(i, j) + end do + end do + end subroutine {fprefix}_2d_array_inout + + function {fprefix}_array_return(o) result (c) + character, dimension(3) :: c + character, intent(in) :: o(3) + do i=1,3 + c(i) = o(i) + end do + end function {fprefix}_array_return + + function {fprefix}_optional(o) result (c) + character, intent(in) :: o + !f2py character o = "a" + character :: c + c = o + end function {fprefix}_optional + """) + + @pytest.mark.parametrize("dtype", ['c', 'S1']) + def test_input(self, dtype): + f = getattr(self.module, self.fprefix + '_input') + + assert_equal(f(np.array('a', dtype=dtype)), ord('a')) + assert_equal(f(np.array(b'a', dtype=dtype)), ord('a')) + assert_equal(f(np.array(['a'], dtype=dtype)), ord('a')) + assert_equal(f(np.array('abc', dtype=dtype)), ord('a')) + assert_equal(f(np.array([['a']], dtype=dtype)), ord('a')) + + def test_input_varia(self): + f = getattr(self.module, self.fprefix + '_input') + + assert_equal(f('a'), ord('a')) + assert_equal(f(b'a'), ord(b'a')) + assert_equal(f(''), 0) + assert_equal(f(b''), 0) + assert_equal(f(b'\0'), 0) + assert_equal(f('ab'), ord('a')) + assert_equal(f(b'ab'), ord('a')) + assert_equal(f(['a']), ord('a')) + + assert_equal(f(np.array(b'a')), ord('a')) + assert_equal(f(np.array([b'a'])), ord('a')) + a = np.array('a') + assert_equal(f(a), ord('a')) + a = np.array(['a']) + assert_equal(f(a), ord('a')) + + try: + f([]) + except IndexError as msg: + if not str(msg).endswith(' got 0-list'): + raise + else: + raise SystemError(f'{f.__name__} should have failed on empty list') + + try: + f(97) + except TypeError as msg: + if not str(msg).endswith(' got int instance'): + raise + else: + raise SystemError(f'{f.__name__} should have failed on int value') + + @pytest.mark.parametrize("dtype", ['c', 'S1', 'U1']) + def test_array_input(self, dtype): + f = getattr(self.module, self.fprefix + '_array_input') + + assert_array_equal(f(np.array(['a', 'b', 'c'], dtype=dtype)), + np.array(list(map(ord, 'abc')), dtype='i1')) + assert_array_equal(f(np.array([b'a', b'b', b'c'], dtype=dtype)), + np.array(list(map(ord, 'abc')), dtype='i1')) + + def test_array_input_varia(self): + f = getattr(self.module, self.fprefix + '_array_input') + assert_array_equal(f(['a', 'b', 'c']), + np.array(list(map(ord, 'abc')), dtype='i1')) + assert_array_equal(f([b'a', b'b', b'c']), + np.array(list(map(ord, 'abc')), dtype='i1')) + + try: + f(['a', 'b', 'c', 'd']) + except ValueError as msg: + if not str(msg).endswith( + 'th dimension must be fixed to 3 but got 4'): + raise + else: + raise SystemError( + f'{f.__name__} should have failed on wrong input') + + @pytest.mark.parametrize("dtype", ['c', 'S1', 'U1']) + def test_2d_array_input(self, dtype): + f = getattr(self.module, self.fprefix + '_2d_array_input') + + a = np.array([['a', 'b', 'c'], + ['d', 'e', 'f']], dtype=dtype, order='F') + expected = a.view(np.uint32 if dtype == 'U1' else np.uint8) + assert_array_equal(f(a), expected) + + def test_output(self): + f = getattr(self.module, self.fprefix + '_output') + + assert_equal(f(ord(b'a')), b'a') + assert_equal(f(0), b'\0') + + def test_array_output(self): + f = getattr(self.module, self.fprefix + '_array_output') + + assert_array_equal(f(list(map(ord, 'abc'))), + np.array(list('abc'), dtype='S1')) + + def test_input_output(self): + f = getattr(self.module, self.fprefix + '_input_output') + + assert_equal(f(b'a'), b'a') + assert_equal(f('a'), b'a') + assert_equal(f(''), b'\0') + + @pytest.mark.parametrize("dtype", ['c', 'S1']) + def test_inout(self, dtype): + f = getattr(self.module, self.fprefix + '_inout') + + a = np.array(list('abc'), dtype=dtype) + f(a, 'A') + assert_array_equal(a, np.array(list('Abc'), dtype=a.dtype)) + f(a[1:], 'B') + assert_array_equal(a, np.array(list('ABc'), dtype=a.dtype)) + + a = np.array(['abc'], dtype=dtype) + f(a, 'A') + assert_array_equal(a, np.array(['Abc'], dtype=a.dtype)) + + def test_inout_varia(self): + f = getattr(self.module, self.fprefix + '_inout') + a = np.array('abc', dtype='S3') + f(a, 'A') + assert_array_equal(a, np.array('Abc', dtype=a.dtype)) + + a = np.array(['abc'], dtype='S3') + f(a, 'A') + assert_array_equal(a, np.array(['Abc'], dtype=a.dtype)) + + try: + f('abc', 'A') + except ValueError as msg: + if not str(msg).endswith(' got 3-str'): + raise + else: + raise SystemError(f'{f.__name__} should have failed on str value') + + @pytest.mark.parametrize("dtype", ['c', 'S1']) + def test_array_inout(self, dtype): + f = getattr(self.module, self.fprefix + '_array_inout') + n = np.array(['A', 'B', 'C'], dtype=dtype, order='F') + + a = np.array(['a', 'b', 'c'], dtype=dtype, order='F') + f(a, n) + assert_array_equal(a, n) + + a = np.array(['a', 'b', 'c', 'd'], dtype=dtype) + f(a[1:], n) + assert_array_equal(a, np.array(['a', 'A', 'B', 'C'], dtype=dtype)) + + a = np.array([['a', 'b', 'c']], dtype=dtype, order='F') + f(a, n) + assert_array_equal(a, np.array([['A', 'B', 'C']], dtype=dtype)) + + a = np.array(['a', 'b', 'c', 'd'], dtype=dtype, order='F') + try: + f(a, n) + except ValueError as msg: + if not str(msg).endswith( + 'th dimension must be fixed to 3 but got 4'): + raise + else: + raise SystemError( + f'{f.__name__} should have failed on wrong input') + + @pytest.mark.parametrize("dtype", ['c', 'S1']) + def test_2d_array_inout(self, dtype): + f = getattr(self.module, self.fprefix + '_2d_array_inout') + n = np.array([['A', 'B', 'C'], + ['D', 'E', 'F']], + dtype=dtype, order='F') + a = np.array([['a', 'b', 'c'], + ['d', 'e', 'f']], + dtype=dtype, order='F') + f(a, n) + assert_array_equal(a, n) + + def test_return(self): + f = getattr(self.module, self.fprefix + '_return') + + assert_equal(f('a'), b'a') + + @pytest.mark.skip('fortran function returning array segfaults') + def test_array_return(self): + f = getattr(self.module, self.fprefix + '_array_return') + + a = np.array(list('abc'), dtype='S1') + assert_array_equal(f(a), a) + + def test_optional(self): + f = getattr(self.module, self.fprefix + '_optional') + + assert_equal(f(), b"a") + assert_equal(f(b'B'), b"B") + + +class TestMiscCharacter(util.F2PyTest): + # options = ['--debug-capi', '--build-dir', '/tmp/test-build-f2py'] + suffix = '.f90' + fprefix = 'test_misc_character' + + code = textwrap.dedent(f""" + subroutine {fprefix}_gh18684(x, y, m) + character(len=5), dimension(m), intent(in) :: x + character*5, dimension(m), intent(out) :: y + integer i, m + !f2py integer, intent(hide), depend(x) :: m = f2py_len(x) + do i=1,m + y(i) = x(i) + end do + end subroutine {fprefix}_gh18684 + + subroutine {fprefix}_gh6308(x, i) + integer i + !f2py check(i>=0 && i<12) i + character*5 name, x + common name(12) + name(i + 1) = x + end subroutine {fprefix}_gh6308 + + subroutine {fprefix}_gh4519(x) + character(len=*), intent(in) :: x(:) + !f2py intent(out) x + integer :: i + do i=1, size(x) + print*, "x(",i,")=", x(i) + end do + end subroutine {fprefix}_gh4519 + + pure function {fprefix}_gh3425(x) result (y) + character(len=*), intent(in) :: x + character(len=len(x)) :: y + integer :: i + do i = 1, len(x) + j = iachar(x(i:i)) + if (j>=iachar("a") .and. j<=iachar("z") ) then + y(i:i) = achar(j-32) + else + y(i:i) = x(i:i) + endif + end do + end function {fprefix}_gh3425 + + subroutine {fprefix}_character_bc_new(x, y, z) + character, intent(in) :: x + character, intent(out) :: y + !f2py character, depend(x) :: y = x + !f2py character, dimension((x=='a'?1:2)), depend(x), intent(out) :: z + character, dimension(*) :: z + !f2py character, optional, check(x == 'a' || x == 'b') :: x = 'a' + !f2py callstatement (*f2py_func)(&x, &y, z) + !f2py callprotoargument character*, character*, character* + if (y.eq.x) then + y = x + else + y = 'e' + endif + z(1) = 'c' + end subroutine {fprefix}_character_bc_new + + subroutine {fprefix}_character_bc_old(x, y, z) + character, intent(in) :: x + character, intent(out) :: y + !f2py character, depend(x) :: y = x[0] + !f2py character, dimension((*x=='a'?1:2)), depend(x), intent(out) :: z + character, dimension(*) :: z + !f2py character, optional, check(*x == 'a' || x[0] == 'b') :: x = 'a' + !f2py callstatement (*f2py_func)(x, y, z) + !f2py callprotoargument char*, char*, char* + if (y.eq.x) then + y = x + else + y = 'e' + endif + z(1) = 'c' + end subroutine {fprefix}_character_bc_old + """) + + def test_gh18684(self): + # Test character(len=5) and character*5 usages + f = getattr(self.module, self.fprefix + '_gh18684') + x = np.array(["abcde", "fghij"], dtype='S5') + y = f(x) + + assert_array_equal(x, y) + + def test_gh6308(self): + # Test character string array in a common block + f = getattr(self.module, self.fprefix + '_gh6308') + + assert_equal(self.module._BLNK_.name.dtype, np.dtype('S5')) + assert_equal(len(self.module._BLNK_.name), 12) + f("abcde", 0) + assert_equal(self.module._BLNK_.name[0], b"abcde") + f("12345", 5) + assert_equal(self.module._BLNK_.name[5], b"12345") + + def test_gh4519(self): + # Test array of assumed length strings + f = getattr(self.module, self.fprefix + '_gh4519') + + for x, expected in [ + ('a', dict(shape=(), dtype=np.dtype('S1'))), + ('text', dict(shape=(), dtype=np.dtype('S4'))), + (np.array(['1', '2', '3'], dtype='S1'), + dict(shape=(3,), dtype=np.dtype('S1'))), + (['1', '2', '34'], + dict(shape=(3,), dtype=np.dtype('S2'))), + (['', ''], dict(shape=(2,), dtype=np.dtype('S1')))]: + r = f(x) + for k, v in expected.items(): + assert_equal(getattr(r, k), v) + + def test_gh3425(self): + # Test returning a copy of assumed length string + f = getattr(self.module, self.fprefix + '_gh3425') + # f is equivalent to bytes.upper + + assert_equal(f('abC'), b'ABC') + assert_equal(f(''), b'') + assert_equal(f('abC12d'), b'ABC12D') + + @pytest.mark.parametrize("state", ['new', 'old']) + def test_character_bc(self, state): + f = getattr(self.module, self.fprefix + '_character_bc_' + state) + + c, a = f() + assert_equal(c, b'a') + assert_equal(len(a), 1) + + c, a = f(b'b') + assert_equal(c, b'b') + assert_equal(len(a), 2) + + assert_raises(Exception, lambda: f(b'c')) diff --git a/numpy/f2py/tests/test_crackfortran.py b/numpy/f2py/tests/test_crackfortran.py index ea618bf33..53edfc091 100644 --- a/numpy/f2py/tests/test_crackfortran.py +++ b/numpy/f2py/tests/test_crackfortran.py @@ -1,3 +1,4 @@ +import codecs import pytest import numpy as np from numpy.f2py.crackfortran import markinnerspaces @@ -18,7 +19,7 @@ class TestNoSpace(util.F2PyTest): assert np.allclose(k, w + 1) self.module.subc([w, k]) assert np.allclose(k, w + 1) - assert self.module.t0(23) == b"2" + assert self.module.t0("23") == b"2" class TestPublicPrivate: @@ -122,7 +123,6 @@ 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. @@ -231,3 +231,30 @@ class TestModuleDeclaration: mod = crackfortran.crackfortran([str(fpath)]) 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 + + assert eval_scalar('123', {}) == '123' + assert eval_scalar('12 + 3', {}) == '15' + assert eval_scalar('a + b', dict(a=1, b=2)) == '3' + assert eval_scalar('"123"', {}) == "'123'" + + +class TestFortranReader(util.F2PyTest): + @pytest.mark.parametrize("encoding", + ['ascii', 'utf-8', 'utf-16', 'utf-32']) + 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) + + """ + subroutine foo() + end subroutine foo + """) + mod = crackfortran.crackfortran([str(f_path)]) + assert mod[0]['name'] == 'foo' diff --git a/numpy/f2py/tests/test_docs.py b/numpy/f2py/tests/test_docs.py new file mode 100644 index 000000000..4aa5f5f5c --- /dev/null +++ b/numpy/f2py/tests/test_docs.py @@ -0,0 +1,55 @@ +import os +import pytest +import numpy as np +from numpy.testing import assert_array_equal, assert_equal +from . import util + + +def get_docdir(): + # assuming that documentation tests are run from a source + # directory + return os.path.abspath(os.path.join( + os.path.dirname(__file__), + '..', '..', '..', + 'doc', 'source', 'f2py')) + + +pytestmark = pytest.mark.skipif( + not os.path.isdir(get_docdir()), + reason=('Could not find f2py documentation sources' + f' ({get_docdir()} does not exists)')) + + +def _path(*a): + return os.path.join(*((get_docdir(),) + a)) + + +class TestDocAdvanced(util.F2PyTest): + # options = ['--debug-capi', '--build-dir', '/tmp/build-f2py'] + sources = [_path('asterisk1.f90'), _path('asterisk2.f90'), + _path('ftype.f')] + + def test_asterisk1(self): + foo = getattr(self.module, 'foo1') + assert_equal(foo(), b'123456789A12') + + def test_asterisk2(self): + foo = getattr(self.module, 'foo2') + assert_equal(foo(2), b'12') + assert_equal(foo(12), b'123456789A12') + assert_equal(foo(24), b'123456789A123456789B') + + def test_ftype(self): + ftype = self.module + ftype.foo() + assert_equal(ftype.data.a, 0) + ftype.data.a = 3 + ftype.data.x = [1, 2, 3] + assert_equal(ftype.data.a, 3) + assert_array_equal(ftype.data.x, + np.array([1, 2, 3], dtype=np.float32)) + ftype.data.x[1] = 45 + assert_array_equal(ftype.data.x, + np.array([1, 45, 3], dtype=np.float32)) + + # TODO: implement test methods for other example Fortran codes diff --git a/numpy/f2py/tests/test_return_character.py b/numpy/f2py/tests/test_return_character.py index 21055faef..36c1f10f4 100644 --- a/numpy/f2py/tests/test_return_character.py +++ b/numpy/f2py/tests/test_return_character.py @@ -10,7 +10,7 @@ IS_S390X = platform.machine() == "s390x" class TestReturnCharacter(util.F2PyTest): def check_function(self, t, tname): if tname in ["t0", "t1", "s0", "s1"]: - assert t(23) == b"2" + assert t("23") == b"2" r = t("ab") assert r == b"a" r = t(array("ab")) diff --git a/numpy/f2py/tests/util.py b/numpy/f2py/tests/util.py index ae81bbfc4..5f815f9d7 100644 --- a/numpy/f2py/tests/util.py +++ b/numpy/f2py/tests/util.py @@ -58,7 +58,7 @@ def get_module_dir(): def get_temp_module_name(): # Assume single-threaded, and the module dir usable only by this thread global _module_num - d = get_module_dir() + get_module_dir() name = "_test_ext_module_%d" % _module_num _module_num += 1 if name in sys.modules: @@ -117,6 +117,8 @@ def build_module(source_files, options=[], skip=[], only=[], module_name=None): if ext in (".f90", ".f", ".c", ".pyf"): f2py_sources.append(dst) + assert f2py_sources + # Prepare options if module_name is None: module_name = get_temp_module_name() @@ -332,7 +334,11 @@ class F2PyTest: only = [] suffix = ".f" module = None - module_name = None + + @property + def module_name(self): + cls = type(self) + return f'_{cls.__module__.rsplit(".",1)[-1]}_{cls.__name__}_ext_module' def setup(self): if sys.platform == "win32": |