From 3342b7bd6d4cd9befad4477a7e57199b0ae8c398 Mon Sep 17 00:00:00 2001 From: daniel Date: Tue, 3 Sep 2024 10:44:31 +0200 Subject: [PATCH 01/12] Feature/docstring add f2numpy_type utility function --- f90wrap/fortran.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/f90wrap/fortran.py b/f90wrap/fortran.py index ca0c9842..aacbe164 100644 --- a/f90wrap/fortran.py +++ b/f90wrap/fortran.py @@ -912,9 +912,9 @@ def normalise_type(typename, kind_map): return type + kind -def fortran_array_type(typename, kind_map): +def f2numpy_type(typename, kind_map): """ - Convert string repr of Fortran type to equivalent numpy array typenum + Convert string repr of Fortran type to equivalent numpy array type """ c_type = f2c_type(typename, kind_map) @@ -936,10 +936,16 @@ def fortran_array_type(typename, kind_map): if c_type not in c_type_to_numpy_type: raise RuntimeError('Unknown C type %s' % c_type) + numpy_type = np.dtype(c_type_to_numpy_type[c_type]) + return numpy_type +def fortran_array_type(typename, kind_map): + """ + Convert string repr of Fortran type to equivalent numpy array typenum + """ # find numpy numerical type code - numpy_type = np.dtype(c_type_to_numpy_type[c_type]).num - return numpy_type + numpy_type_num = f2numpy_type(typename, kind_map).num + return numpy_type_num def f2py_type(type, attributes=None): """ From 8f953190c4d42cb800d77687e46130426dfa70bf Mon Sep 17 00:00:00 2001 From: daniel Date: Tue, 3 Sep 2024 10:47:09 +0200 Subject: [PATCH 02/12] Feature/docstring Update doc string feature to better match python convention --- examples/CMakeLists.txt | 1 + examples/docstring/Makefile | 6 +- examples/docstring/docstring_test.py | 111 ++++++++++------- examples/docstring/main.f90 | 17 ++- f90wrap/parser.py | 15 ++- f90wrap/pywrapgen.py | 174 +++++++++++++++------------ 6 files changed, 188 insertions(+), 136 deletions(-) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index c27c3154..ed0bdcde 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -26,6 +26,7 @@ list(APPEND tests subroutine_contains_issue101 type_bn kind_map_default + docstring ) foreach(test ${tests}) diff --git a/examples/docstring/Makefile b/examples/docstring/Makefile index aa88c9fb..b5a28133 100644 --- a/examples/docstring/Makefile +++ b/examples/docstring/Makefile @@ -31,12 +31,8 @@ main.o: ${F90_SRC} ${F90WRAP_SRC}: ${OBJ} ${F90WRAP} -m ${PY_MOD} ${WRAPFLAGS} ${F90_SRC} -f90wrap: ${F90WRAP_SRC} - f2py: ${F90WRAP_SRC} CFLAGS="${CFLAGS}" ${F2PY} -c -m _${PY_MOD} ${F2PYFLAGS} f90wrap_*.f90 *.o -wrapper: f2py - -test: wrapper +test: f2py python docstring_test.py diff --git a/examples/docstring/docstring_test.py b/examples/docstring/docstring_test.py index d5b8fad6..e7531d2d 100644 --- a/examples/docstring/docstring_test.py +++ b/examples/docstring/docstring_test.py @@ -21,34 +21,55 @@ def test_module_doc(self): circle = m_circle.t_circle() docstring = m_circle.__doc__ ref_docstring = """ - Module m_circle - - - Defined at main.f90 lines 7-89 - File: main.f90 - Brief: Test program docstring + Test program docstring + Author: test_author Copyright: test_copyright + + Module m_circle + Defined at main.f90 lines 7-89 """ assert clean_str(ref_docstring) == clean_str(docstring) - def test_docstring(self): + def test_subroutine_docstring(self): circle = m_circle.t_circle() docstring = m_circle.construct_circle.__doc__ ref_docstring = """ + Initialize circle + construct_circle(self, radius) + Defined at main.f90 lines 17-20 + Parameters + ---------- + circle : T_Circle + t_circle to initialize [in,out] + radius : float32 + radius of the circle [in] + """ + + assert clean_str(ref_docstring) == clean_str(docstring) + + def test_subroutine_docstring_more_doc(self): + circle = m_circle.t_circle() + docstring = m_circle.construct_circle_more_doc.__doc__ + ref_docstring = """ + Initialize circle with more doc + + Author: test_author + Copyright: test_copyright + construct_circle_more_doc(self, radius) Defined at main.f90 lines 17-20 Parameters ---------- - circle : T_Circle, [in,out] t_circle to initialize - radius : float, [in] radius of the circle - - Brief: Initialize circle + circle : T_Circle + t_circle to initialize [in,out] + radius : float32 + radius of the circle [in] """ assert clean_str(ref_docstring) == clean_str(docstring) @@ -57,17 +78,17 @@ def test_no_direction(self): circle = m_circle.t_circle() docstring = m_circle.no_direction.__doc__ ref_docstring = """ - no_direction(self, radius) - + Without direction + no_direction(self, radius) Defined at main.f90 lines 28-31 Parameters ---------- - circle : T_Circle, t_circle to initialize - radius : float, radius of the circle - - Brief: Without direction + circle : T_Circle + t_circle to initialize + radius : float32 + radius of the circle """ assert clean_str(ref_docstring) == clean_str(docstring) @@ -76,17 +97,16 @@ def test_docstring_incomplet(self): circle = m_circle.t_circle() docstring = m_circle.incomplete_doc_sub.__doc__ ref_docstring = """ - incomplete_doc_sub(self, radius) - + Incomplete doc + incomplete_doc_sub(self, radius) Defined at main.f90 lines 38-41 Parameters ---------- circle : T_Circle - radius : float, [in] radius of the circle - - Brief: Incomplete doc + radius : float32 + radius of the circle [in] """ assert clean_str(ref_docstring) == clean_str(docstring) @@ -95,17 +115,15 @@ def test_param_return(self): circle = m_circle.t_circle() docstring = m_circle.output_1.__doc__ ref_docstring = """ - output = output_1() - + subroutine output_1 outputs 1 + output = output_1() Defined at main.f90 lines 59-61 - Returns ------- - output : float, [out] this is 1 - - Brief: subroutine output_1 outputs 1 + output : float32 + this is 1 [out] """ assert clean_str(ref_docstring) == clean_str(docstring) @@ -114,20 +132,20 @@ def test_function_return(self): circle = m_circle.t_circle() docstring = m_circle.function_2.__doc__ ref_docstring = """ - function_2 = function_2(input) - + this is a function + function_2 = function_2(input) Defined at main.f90 lines 69-71 Parameters ---------- - input : str, [in] value + input : str + value [in] Returns ------- - function_2 : int, return value - - Brief: this is a function + function_2 : int32 + return value """ assert clean_str(ref_docstring) == clean_str(docstring) @@ -136,18 +154,19 @@ def test_details(self): circle = m_circle.t_circle() docstring = m_circle.details_doc.__doc__ ref_docstring = """ - details_doc(self, radius) + Initialize circle + Those are very informative details + details_doc(self, radius) Defined at main.f90 lines 80-82 Parameters ---------- - circle : T_Circle, [in,out] t_circle to initialize - radius : float, [in] radius of the circle - - Brief: Initialize circle - Details: Those are very informative details + circle : T_Circle + t_circle to initialize [in,out] + radius : float32 + radius of the circle [in] """ assert clean_str(ref_docstring) == clean_str(docstring) @@ -157,17 +176,17 @@ def test_doc_inside(self): circle = m_circle.t_circle() docstring = m_circle.doc_inside.__doc__ ref_docstring = """ - doc_inside(self, radius) - + Doc inside + doc_inside(self, radius) Defined at main.f90 lines 43-52 Parameters ---------- - circle : T_Circle, [in,out] t_circle to initialize - radius : float, [in] radius of the circle - - Brief: Doc inside + circle : T_Circle + t_circle to initialize [in,out] + radius : float32 + radius of the circle [in] """ assert clean_str(ref_docstring) == clean_str(docstring) diff --git a/examples/docstring/main.f90 b/examples/docstring/main.f90 index 1525b850..8407c53d 100644 --- a/examples/docstring/main.f90 +++ b/examples/docstring/main.f90 @@ -12,7 +12,8 @@ module m_circle real :: radius end type t_circle - public :: construct_circle,incomplete_doc_sub + public :: construct_circle,construct_circle_more_doc + public :: incomplete_doc_sub public :: no_direction,doc_inside public :: output_1,function_2,details_doc @@ -30,6 +31,20 @@ subroutine construct_circle(circle,radius) circle%radius = radius end subroutine construct_circle + !=========================================================================== + !> + !! \brief Initialize circle with more doc + !! \author test_author + !! \copyright test_copyright + !! \param[in,out] circle t_circle to initialize + !! \param[in] radius radius of the circle + !< + subroutine construct_circle_more_doc(circle,radius) + type(t_circle) :: circle + real, intent(in) :: radius + circle%radius = radius + end subroutine construct_circle_more_doc + !=========================================================================== !> !! \brief Without direction diff --git a/f90wrap/parser.py b/f90wrap/parser.py index 90ebcfbd..8009f2b4 100644 --- a/f90wrap/parser.py +++ b/f90wrap/parser.py @@ -127,7 +127,8 @@ fdoc_mark = re.compile('_FD\s*') fdoc_rv_mark = re.compile('_FDRV\s*') -doxygen_keys = re.compile('_COMMENT.*\\\\(brief|details|file|author|copyright)') +doxygen_main = re.compile('_COMMENT.*\\\\(brief|details)') +doxygen_others = re.compile('_COMMENT.*\\\\(file|author|copyright)') doxygen_param = re.compile('_COMMENT.*\\\\(param|returns)') doxygen_param_group = re.compile('_COMMENT.*\\\\(param|returns)\s*(\[.*?\]|)\s*(\S*)\s*(.*)') @@ -349,18 +350,20 @@ def check_uses(cline, file): def check_doc(cline, file): out = None if cline: - for pattern in [fdoc_mark, doxygen_keys, doxygen_param]: + for pattern in [fdoc_mark, doxygen_main, doxygen_others, doxygen_param]: match = re.search(pattern, cline) if match != None: if pattern == doxygen_param: # Leave pattern for later parsing in check_arg - out = cline - elif pattern == doxygen_keys: + out = cline.strip() + elif pattern == doxygen_main: + key = match.group(1) + out = pattern.sub('', cline).strip(' ') + '\n' + elif pattern == doxygen_others: key = match.group(1) out = key.capitalize() + ': ' + pattern.sub('', cline).strip(' ') else: out = pattern.sub('', cline).strip(' ') - out = out.rstrip() cline = file.next() return [out, cline] return [out, cline] @@ -1471,7 +1474,7 @@ def check_arg(cl, file): comm = match.group(4) if name in nl: hold_doc.remove(line) - doxygen_map[name] = ' '.join([direction, comm]).strip(' ') + doxygen_map[name] = ' '.join([comm, direction]).strip(' ') dc = [] diff --git a/f90wrap/pywrapgen.py b/f90wrap/pywrapgen.py index 497df048..e0117c27 100644 --- a/f90wrap/pywrapgen.py +++ b/f90wrap/pywrapgen.py @@ -92,71 +92,6 @@ def format_call_signature(node): else: return str(node) - -def format_doc_string(node): - """ - Generate Python docstring from Fortran docstring and call signature - """ - - def _format_line_no(lineno): - """ - Format Fortran source code line numbers - - FIXME could link to source repository (e.g. github) - """ - if isinstance(lineno, slice): - return "lines %d-%d" % (lineno.start, lineno.stop - 1) - else: - return "line %d" % lineno - - doc = [format_call_signature(node), ""] - doc.append("") - doc.append("Defined at %s %s" % (node.filename, _format_line_no(node.lineno))) - - if isinstance(node, ft.Procedure): - # For procedures, write parameters and return values in numpydoc format - doc.append("") - # Input parameters - for i, arg in enumerate(node.arguments): - pytype = ft.f2py_type(arg.type, arg.attributes) - if i == 0: - doc.append("Parameters") - doc.append("----------") - arg_doc = "%s : %s, %s" % (arg.name, pytype, arg.doxygen) - doc.append(arg_doc.strip(", ")) - if arg.doc: - for d in arg.doc: - doc.append("\t%s" % d) - doc.append("") - - if isinstance(node, ft.Function): - for i, arg in enumerate(node.ret_val): - pytype = ft.f2py_type(arg.type, arg.attributes) - if i == 0: - doc.append("") - doc.append("Returns") - doc.append("-------") - arg_doc = "%s : %s, %s" % (arg.name, pytype, arg.doxygen) - doc.append(arg_doc.strip(", ")) - if arg.doc: - for d in arg.doc: - doc.append("\t%s" % d) - doc.append("") - elif isinstance(node, ft.Interface): - # for interfaces, list the components - doc.append("") - doc.append("Overloaded interface containing the following procedures:") - for proc in node.procedures: - doc.append( - " %s" - % (hasattr(proc, "method_name") and proc.method_name or proc.name) - ) - - doc += [""] + node.doc[:] # incoming docstring from Fortran source - - return "\n".join(['"""'] + doc + ['"""']) - - class PythonWrapperGenerator(ft.FortranVisitor, cg.CodeGenerator): def __init__( self, @@ -258,11 +193,11 @@ def visit_Module(self, node): if self.make_package: self.code = [] - self.write(format_doc_string(node)) + self.write(self._format_doc_string(node)) else: self.write("class %s(f90wrap.runtime.FortranModule):" % cls_name) self.indent() - self.write(format_doc_string(node)) + self.write(self._format_doc_string(node)) if ( len(node.elements) == 0 @@ -376,7 +311,7 @@ def write_constructor(self, node): self.write("def __init__(self, %(py_arg_names)s):" % dct) self.indent() - self.write(format_doc_string(node)) + self.write(self._format_doc_string(node)) for arg in node.arguments: if "optional" in arg.attributes and "._handle" in arg.py_value: dct["f90_arg_names"] = dct["f90_arg_names"].replace( @@ -424,7 +359,7 @@ def write_classmethod(self, node): self.write("@classmethod") self.write("def %(method_name)s(cls, %(py_arg_names)s):" % dct) self.indent() - self.write(format_doc_string(node)) + self.write(self._format_doc_string(node)) self.write("bare_class = cls.__new__(cls)") self.write("f90wrap.runtime.FortranDerivedType.__init__(bare_class)") @@ -452,7 +387,7 @@ def write_destructor(self, node): self.write("def __del__(%(py_arg_names)s):" % dct) self.indent() - self.write(format_doc_string(node)) + self.write(self._format_doc_string(node)) self.write("if self._alloc:") self.indent() self.write('%(mod_name)s.%(subroutine_name)s(%(f90_arg_names)s)' % dct) @@ -501,7 +436,7 @@ def visit_Procedure(self, node): self.write("@staticmethod") self.write("def %(method_name)s(%(py_arg_names)s):" % dct) self.indent() - self.write(format_doc_string(node)) + self.write(self._format_doc_string(node)) if self.type_check: self.write_type_checks(node) @@ -574,7 +509,7 @@ def visit_Interface(self, node): self.write("@staticmethod") self.write("def %(intf_name)s(*args, **kwargs):" % dct) self.indent() - self.write(format_doc_string(node)) + self.write(self._format_doc_string(node)) # try to call each in turn until no TypeError raised self.write("for proc in %(proc_names)s:" % dct) self.indent() @@ -623,7 +558,7 @@ def visit_Type(self, node): ) self.write("class %s(f90wrap.runtime.FortranDerivedType):" % cls_name) self.indent() - self.write(format_doc_string(node)) + self.write(self._format_doc_string(node)) self.generic_visit(node) properties = [] @@ -694,7 +629,7 @@ def write_scalar_wrappers(self, node, el, properties): self.write("def %(el_name_get)s(%(self)s):" % dct) self.indent() - self.write(format_doc_string(el)) + self.write(self._format_doc_string(el)) self.write('return %(mod_name)s.%(subroutine_name_get)s(%(handle)s)' % dct) self.dedent() self.write() @@ -779,7 +714,7 @@ def write_dt_wrappers(self, node, el, properties): self.write("def %(el_name_get)s(%(self)s):" % dct) self.indent() - self.write(format_doc_string(el)) + self.write(self._format_doc_string(el)) if isinstance(node, ft.Module) and self.make_package: self.write("global %(el_name)s" % dct) self.write( @@ -819,7 +754,7 @@ def write_sc_array_wrapper(self, node, el, dims, properties): self="self", selfdot="self.", selfcomma="self, ", - doc=format_doc_string(el), + doc=self._format_doc_string(el), handle=isinstance(node, ft.Type) and "self._handle" or "f90wrap.runtime.empty_handle", @@ -837,7 +772,7 @@ def write_sc_array_wrapper(self, node, el, dims, properties): self.write("def %(el_name_get)s(%(self)s):" % dct) self.indent() - self.write(format_doc_string(el)) + self.write(self._format_doc_string(el)) if isinstance(node, ft.Module) and self.make_package: self.write("global %(el_name)s" % dct) node.array_initialisers.append(dct["el_name_get"]) @@ -902,7 +837,7 @@ def write_dt_array_wrapper(self, node, el, dims): self="self", selfdot="self.", parent="self", - doc=format_doc_string(el), + doc=self._format_doc_string(el), cls_name=cls_name, cls_mod_name=normalise_class_name(cls_mod_name, self.class_names) + ".", ) @@ -1032,3 +967,86 @@ def write_type_checks(self, node): self.indent() self.write("raise TypeError") self.dedent() + + def _format_doc_string(self, node): + """ + Generate Python docstring from Fortran docstring and call signature + """ + + def _format_line_no(lineno): + """ + Format Fortran source code line numbers + + FIXME could link to source repository (e.g. github) + """ + if isinstance(lineno, slice): + return "lines %d-%d" % (lineno.start, lineno.stop - 1) + else: + return "line %d" % lineno + + def _format_pytype(self, arg): + pytype = ft.f2py_type(arg.type, arg.attributes) + if pytype in ["float", "int", "complex"]: + # This allows to specify size, ex: 32 bit, 64 bit + pytype = ft.f2numpy_type(arg.type, self.kind_map) + return pytype + + doc = node.doc[:] # incoming docstring from Fortran source + if ( + doc and doc[-1][-1] != "\n" + ): # Short sumary and extended sumary have a trailing newline + doc.append("") + doc.append(format_call_signature(node)) + doc.append("Defined at %s %s" % (node.filename, _format_line_no(node.lineno))) + + if isinstance(node, ft.Procedure): + # For procedures, write parameters and return values in numpydoc format + doc.append("") + # Input parameters + for i, arg in enumerate(node.arguments): + pytype = _format_pytype(self, arg) + if i == 0: + doc.append("Parameters") + doc.append("----------") + arg_doc = "%s : %s\n%s%s" % ( + arg.name, + pytype, + self._indent, + arg.doxygen, + ) + doc.append(arg_doc.strip(", \n%s" % self._indent)) + if arg.doc: + for d in arg.doc: + doc.append("%s%s" % (self._indent, d)) + doc.append("") + + if isinstance(node, ft.Function): + for i, arg in enumerate(node.ret_val): + pytype = _format_pytype(self, arg) + if i == 0: + if doc[-1] != "": + doc.append("") + doc.append("Returns") + doc.append("-------") + arg_doc = "%s : %s\n%s%s" % ( + arg.name, + pytype, + self._indent, + arg.doxygen, + ) + doc.append(arg_doc.strip(", \n%s" % self._indent)) + if arg.doc: + for d in arg.doc: + doc.append("%s%s" % (self._indent, d)) + doc.append("") + elif isinstance(node, ft.Interface): + # for interfaces, list the components + doc.append("") + doc.append("Overloaded interface containing the following procedures:") + for proc in node.procedures: + doc.append( + " %s" + % (hasattr(proc, "method_name") and proc.method_name or proc.name) + ) + + return "\n".join(['"""'] + doc + ['"""']) From 9fc2d721858b46e492ae78e99723f35dd4f8ba8d Mon Sep 17 00:00:00 2001 From: daniel Date: Tue, 3 Sep 2024 11:16:33 +0200 Subject: [PATCH 03/12] Move split_dimensions and add dims_list in argument class --- f90wrap/f90wrapgen.py | 5 ++--- f90wrap/fortran.py | 32 +++++++++++++++++++++++++++++++- f90wrap/pywrapgen.py | 6 +----- f90wrap/transform.py | 30 +++++------------------------- 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/f90wrap/f90wrapgen.py b/f90wrap/f90wrapgen.py index 69c527e8..4a56dc34 100644 --- a/f90wrap/f90wrapgen.py +++ b/f90wrap/f90wrapgen.py @@ -30,7 +30,6 @@ from f90wrap import codegen as cg from f90wrap import fortran as ft from f90wrap.six import string_types # Python 2/3 compatibility library -from f90wrap.transform import ArrayDimensionConverter from f90wrap.transform import shorten_long_name log = logging.getLogger(__name__) @@ -567,7 +566,7 @@ def _write_sc_array_wrapper(self, t, el, dims, sizeof_fortran_t): self.write("integer(c_int), intent(out) :: nd") self.write("integer(c_int), intent(out) :: dtype") try: - rank = len(ArrayDimensionConverter.split_dimensions(dims)) + rank = len(ft.Argument.split_dimensions(dims)) if el.type.startswith("character"): rank += 1 except ValueError: @@ -627,7 +626,7 @@ def _write_dt_array_wrapper(self, t, element, dims, sizeof_fortran_t): """ if ( element.type.startswith("type") - and len(ArrayDimensionConverter.split_dimensions(dims)) != 1 + and len(ft.Argument.split_dimensions(dims)) != 1 ): return diff --git a/f90wrap/fortran.py b/f90wrap/fortran.py index aacbe164..ff6e0bce 100644 --- a/f90wrap/fortran.py +++ b/f90wrap/fortran.py @@ -308,7 +308,37 @@ class Element(Declaration): class Argument(Declaration): __doc__ = _rep_des(Declaration.__doc__, "Represents a Procedure Argument.") - pass + + @staticmethod + def split_dimensions(dim): + """Given a string like "dimension(a,b,c)" return the list of dimensions ['a','b','c'].""" + dim = dim[10:-1] # remove "dimension(" and ")" + br = 0 + d = 1 + ds = [''] + for c in dim: + if c != ',': ds[-1] += c + if c == '(': + br += 1 + elif c == ')': + br -= 1 + elif c == ',': + if br == 0: + ds.append('') + else: + ds[-1] += ',' + return ds + + def dims_list(self): + dims = list(filter(lambda x: x.startswith("dimension"), self.attributes)) + if len(dims) > 1: + raise ValueError('more than one dimension attribute found for arg %s:\\\n%s' % (self.name, ','.join(dims))) + try: + ft_array_dims_list = self.split_dimensions(dims[0]) + except IndexError: + ft_array_dims_list = [] + ft_array_dims_list = [elem.strip(' ') for elem in ft_array_dims_list] + return ft_array_dims_list class Type(Fortran): """ diff --git a/f90wrap/pywrapgen.py b/f90wrap/pywrapgen.py index e0117c27..477d1892 100644 --- a/f90wrap/pywrapgen.py +++ b/f90wrap/pywrapgen.py @@ -25,7 +25,6 @@ import logging import re -from f90wrap.transform import ArrayDimensionConverter from f90wrap.transform import shorten_long_name from f90wrap import fortran as ft from f90wrap import codegen as cg @@ -815,10 +814,7 @@ def write_sc_array_wrapper(self, node, el, dims, properties): self.write() def write_dt_array_wrapper(self, node, el, dims): - if ( - el.type.startswith("type") - and len(ArrayDimensionConverter.split_dimensions(dims)) != 1 - ): + if el.type.startswith("type") and len(ft.Argument.split_dimensions(dims)) != 1: return func_name = "init_array_%s" % el.name diff --git a/f90wrap/transform.py b/f90wrap/transform.py index ef832120..9fd086e6 100644 --- a/f90wrap/transform.py +++ b/f90wrap/transform.py @@ -282,7 +282,7 @@ def visit_Procedure(self, node): # return none if len(dims) > 1: raise ValueError('more than one dimension attribute found for arg %s' % arg.name) - dimensions_list = ArrayDimensionConverter.split_dimensions(dims[0]) + dimensions_list = arg.split_dimensions(dims[0]) if len(dimensions_list) > 1 or ':' in dimensions_list: log.warning('removing routine %s due to derived type array argument : %s -- currently, only ' 'fixed-lengh one-dimensional arrays of derived type are supported' @@ -328,7 +328,7 @@ def visit_Argument(self, node): if typename and len(dims) != 0: if len(dims) > 1: raise ValueError('more than one dimension attribute found for arg %s' % node.name) - dimensions_list = ArrayDimensionConverter.split_dimensions(dims[0]) + dimensions_list = ft.Argument.split_dimensions(dims[0]) if len(dimensions_list) > 1 or ':' in dimensions_list: log.warning( 'test removing optional argument %s as only one dimensional fixed-length arrays are currently supported for derived type %s array' % @@ -577,26 +577,6 @@ class ArrayDimensionConverter(ft.FortranVisitor): valid_dim_re = re.compile(r'^(([-0-9.e]+)|(size\([_a-zA-Z0-9\+\-\*\/,]*\))|(len\(.*\)))$') - @staticmethod - def split_dimensions(dim): - """Given a string like "dimension(a,b,c)" return the list of dimensions ['a','b','c'].""" - dim = dim[10:-1] # remove "dimension(" and ")" - br = 0 - d = 1 - ds = [''] - for c in dim: - if c != ',': ds[-1] += c - if c == '(': - br += 1 - elif c == ')': - br -= 1 - elif c == ',': - if br == 0: - ds.append('') - else: - ds[-1] += ',' - return ds - def visit_Procedure(self, node): n_dummy = 0 @@ -607,7 +587,7 @@ def visit_Procedure(self, node): if len(dims) != 1: raise ValueError('more than one dimension attribute found for arg %s' % arg.name) - ds = ArrayDimensionConverter.split_dimensions(dims[0]) + ds = arg.split_dimensions(dims[0]) new_dummy_args = [] new_ds = [] @@ -1415,7 +1395,7 @@ def create_super_types(tree, types): for ty in types.values(): for dimensions_attribute in ty.super_types_dimensions: # each type might have many "dimension" attributes since "append_type_dimension" - dimensions = ArrayDimensionConverter.split_dimensions(dimensions_attribute) + dimensions = ft.Argument.split_dimensions(dimensions_attribute) if len(dimensions) == 1: # at this point, only 1D arrays are supported d = dimensions[0] if str(d) == ':': @@ -1464,7 +1444,7 @@ def fix_subroutine_type_arrays(tree, types): if ft.is_derived_type(arg.type) and len(dimensions_attribute) == 1: # an argument should only have 0 or 1 "dimension" attributes # If the argument is an 1D-array of types, convert it to super-type: - d = ArrayDimensionConverter.split_dimensions(dimensions_attribute[0])[0] + d = ft.Argument.split_dimensions(dimensions_attribute[0])[0] if str(d) == ':': continue # change the type to super-type From 5fc2dcfefe4c0c7bf81b5a2f7e52a8233d672bd3 Mon Sep 17 00:00:00 2001 From: daniel Date: Tue, 3 Sep 2024 11:20:18 +0200 Subject: [PATCH 04/12] Fix handling of return array --- examples/CMakeLists.txt | 1 + examples/Makefile | 1 + examples/return_array/Makefile | 38 +++++++ examples/return_array/Makefile.meson | 6 + examples/return_array/main.f90 | 164 +++++++++++++++++++++++++++ examples/return_array/test.py | 135 ++++++++++++++++++++++ f90wrap/pywrapgen.py | 57 ++++++++++ f90wrap/transform.py | 28 ++++- 8 files changed, 424 insertions(+), 6 deletions(-) create mode 100644 examples/return_array/Makefile create mode 100644 examples/return_array/Makefile.meson create mode 100644 examples/return_array/main.f90 create mode 100644 examples/return_array/test.py diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index ed0bdcde..d39f1665 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -27,6 +27,7 @@ list(APPEND tests type_bn kind_map_default docstring + return_array ) foreach(test ${tests}) diff --git a/examples/Makefile b/examples/Makefile index a3cac481..563a92fe 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -23,6 +23,7 @@ EXAMPLES = arrayderivedtypes \ docstring \ type_check \ derivedtypes_procedure \ + return_array \ optional_string \ long_subroutine_name \ kind_map_default diff --git a/examples/return_array/Makefile b/examples/return_array/Makefile new file mode 100644 index 00000000..c2e87712 --- /dev/null +++ b/examples/return_array/Makefile @@ -0,0 +1,38 @@ +#======================================================================= +# define the compiler names +#======================================================================= + +CC = gcc +F90 = gfortran +PYTHON = python +CFLAGS = -fPIC +F90FLAGS = -fPIC +PY_MOD = pywrapper +F90_SRC = main.f90 +OBJ = $(F90_SRC:.f90=.o) +F90WRAP_SRC = $(addprefix f90wrap_,${F90_SRC}) +WRAPFLAGS = -v +F2PYFLAGS = --build-dir build +F90WRAP = f90wrap +F2PY = f2py-f90wrap +.PHONY: all clean + +all: test + +clean: + rm -rf *.mod *.smod *.o f90wrap*.f90 ${PY_MOD}.py _${PY_MOD}*.so __pycache__/ .f2py_f2cmap build ${PY_MOD}/ + +main.o: ${F90_SRC} + ${F90} ${F90FLAGS} -c $< -o $@ + +%.o: %.f90 + ${F90} ${F90FLAGS} -c $< -o $@ + +${F90WRAP_SRC}: ${OBJ} + ${F90WRAP} -m ${PY_MOD} ${WRAPFLAGS} ${F90_SRC} + +f2py: ${F90WRAP_SRC} + CFLAGS="${CFLAGS}" ${F2PY} -c -m _${PY_MOD} ${F2PYFLAGS} f90wrap_*.f90 *.o + +test: f2py + ${PYTHON} test.py diff --git a/examples/return_array/Makefile.meson b/examples/return_array/Makefile.meson new file mode 100644 index 00000000..b2ee9928 --- /dev/null +++ b/examples/return_array/Makefile.meson @@ -0,0 +1,6 @@ +include ../make.meson.inc + +NAME := pywrapper + +test: build + $(PYTHON) tests.py diff --git a/examples/return_array/main.f90 b/examples/return_array/main.f90 new file mode 100644 index 00000000..7f8c231c --- /dev/null +++ b/examples/return_array/main.f90 @@ -0,0 +1,164 @@ +module m_test + implicit none + private + + type, public :: t_array_wrapper + integer :: a_size + real,allocatable :: a_data(:) + end type t_array_wrapper + + type, public :: t_array_2d_wrapper + integer :: a_size_x, a_size_y + real,allocatable :: a_data(:,:) + end type t_array_2d_wrapper + + type, public :: t_array_double_wrapper + type(t_array_wrapper) array_wrapper + end type t_array_double_wrapper + + type, public :: t_value + real :: value + end type t_value + + type, public :: t_size_2d + integer :: x, y + end type t_size_2d + + public :: array_init, array_free + public :: array_wrapper_init + public :: array_2d_init + public :: return_scalar + public :: return_hard_coded_1d + public :: return_hard_coded_2d + public :: return_array_member + public :: return_array_member_2d + public :: return_array_member_wrapper + public :: return_array_input + public :: return_array_input_2d + public :: return_array_size + public :: return_array_size_2d_in + public :: return_array_size_2d_out + public :: return_derived_type_value + +contains + + subroutine array_init(in_array, in_size) + type(t_array_wrapper), intent(inout) :: in_array + integer, intent(in) :: in_size + + in_array%a_size = in_size + allocate(in_array%a_data(in_array%a_size)) + in_array%a_data = 1 + end subroutine array_init + + subroutine array_2d_init(in_array, in_size_x, in_size_y) + type(t_array_2d_wrapper), intent(inout) :: in_array + integer, intent(in) :: in_size_x, in_size_y + + in_array%a_size_x = in_size_x + in_array%a_size_y = in_size_y + allocate(in_array%a_data(in_array%a_size_x, in_array%a_size_y)) + in_array%a_data = 2 + end subroutine array_2d_init + + subroutine array_wrapper_init(in_wrapper, in_size) + type(t_array_double_wrapper), intent(inout) :: in_wrapper + integer, intent(in) :: in_size + + in_wrapper%array_wrapper%a_size = in_size + allocate(in_wrapper%array_wrapper%a_data(in_wrapper%array_wrapper%a_size)) + in_wrapper%array_wrapper%a_data = 2 + end subroutine array_wrapper_init + + subroutine array_free(in_array) + type(t_array_wrapper), intent(inout) :: in_array + + in_array%a_size = 0 + deallocate(in_array%a_data) + end subroutine array_free + + function return_scalar(in_array) + type(t_array_wrapper), intent(inout) :: in_array + real :: return_scalar + + return_scalar=in_array%a_data(1) + end function return_scalar + + function return_hard_coded_1d() result(retval) + real :: retval(10) + + retval=2 + end function return_hard_coded_1d + + function return_hard_coded_2d() result(retval) + real :: retval(5,6) + + retval=3 + end function return_hard_coded_2d + + function return_array_member(in_array) result(retval) + type(t_array_wrapper), intent(inout) :: in_array + real :: retval(in_array%a_size) + + retval=in_array%a_data + end function return_array_member + + function return_array_member_2d(in_array) result(retval) + type(t_array_2d_wrapper), intent(inout) :: in_array + real :: retval(in_array%a_size_x, in_array%a_size_y) + + retval=in_array%a_data + end function return_array_member_2d + + function return_array_member_wrapper(in_wrapper) result(retval) + type(t_array_double_wrapper), intent(inout) :: in_wrapper + real :: retval(in_wrapper%array_wrapper%a_size) + + retval=in_wrapper%array_wrapper%a_data + end function return_array_member_wrapper + + function return_array_input(in_len) result(retval) + integer, intent(in) :: in_len + real :: retval(in_len) + + retval = 1 + end function return_array_input + + function return_array_input_2d(in_len_x, in_len_y) result(retval) + integer, intent(in) :: in_len_x,in_len_y + real :: retval(in_len_x, in_len_y) + + retval = 2 + end function return_array_input_2d + + function return_array_size(in_array) result(retval) + real, intent(in) :: in_array(:) + real :: retval(size(in_array)) + + retval = 1 + end function return_array_size + + function return_array_size_2d_in(in_array) result(retval) + real, intent(in) :: in_array(:,:) + real :: retval(size(in_array,2)) + + retval = 1 + end function return_array_size_2d_in + + function return_array_size_2d_out(in_array_1, in_array_2) result(retval) + real, intent(in) :: in_array_1(:,:) + real, intent(in) :: in_array_2(:,:) + real :: retval(size(in_array_1,1), size(in_array_2,2)) + + retval = 2 + end function return_array_size_2d_out + + function return_derived_type_value(this,size_2d) result(output) + type(t_value), intent(in) :: this + type(t_size_2d), intent(in) :: size_2d + real :: output(size_2d%x,size_2d%y) + + output = this%value + end function return_derived_type_value + +end module m_test diff --git a/examples/return_array/test.py b/examples/return_array/test.py new file mode 100644 index 00000000..e547de85 --- /dev/null +++ b/examples/return_array/test.py @@ -0,0 +1,135 @@ +import unittest +import numpy as np + +from pywrapper import m_test + +class TestReturnArray(unittest.TestCase): + + def __init__(self, *args, **kwargs): + super(TestReturnArray, self).__init__(*args, **kwargs) + self._shape = (2,) + self._array = m_test.t_array_wrapper() + m_test.array_init(self._array, *self._shape) + + + def test_init(self): + array = m_test.t_array_wrapper() + m_test.array_init(array, 2) + + def test_free(self): + array = m_test.t_array_wrapper() + m_test.array_init(array, 2) + m_test.array_free(array) + + def test_return_scalar(self): + out_scalar = m_test.return_scalar(self._array) + + assert(isinstance(out_scalar, float)) + assert(out_scalar == 1) + + def test_return_hard_coded_1d(self): + out_array = m_test.return_hard_coded_1d() + + assert(isinstance(out_array, np.ndarray)) + assert(out_array.shape == (10,)) + assert((out_array == 2).all) + + def test_return_hard_coded_2d(self): + out_array = m_test.return_hard_coded_2d() + + assert(isinstance(out_array, np.ndarray)) + assert(out_array.shape == (5,6)) + assert((out_array == 3).all) + + def test_return_array_member(self): + out_array = m_test.return_array_member(self._array) + + assert(isinstance(out_array, np.ndarray)) + assert(out_array.shape == self._shape) + assert((out_array == 1).all) + + def test_return_array_member_2d(self): + shape = (3,4) + array_2d = m_test.t_array_2d_wrapper() + m_test.array_2d_init(array_2d, *shape) + + out_array = m_test.return_array_member_2d(array_2d) + + assert(isinstance(out_array, np.ndarray)) + assert(out_array.shape == shape) + assert((out_array == 2).all) + + def test_return_array_member_wrapper(self): + shape = (3,) + array_wrapper = m_test.t_array_double_wrapper() + m_test.array_wrapper_init(array_wrapper, *shape) + + out_array = m_test.return_array_member_wrapper(array_wrapper) + + assert(isinstance(out_array, np.ndarray)) + assert(out_array.shape == shape) + assert((out_array == 2).all) + + def test_return_array_input(self): + shape = (4,) + out_array = m_test.return_array_input(*shape) + + assert(isinstance(out_array, np.ndarray)) + assert(out_array.shape == shape) + assert((out_array == 1).all) + + def test_return_array_input_2d(self): + shape = (5,4) + out_array = m_test.return_array_input_2d(*shape) + + assert(isinstance(out_array, np.ndarray)) + assert(out_array.shape == shape) + assert((out_array == 2).all) + + def test_return_array_size(self): + shape = (2,) + in_array = np.zeros(shape, dtype=np.float32, order="F") + out_array = m_test.return_array_size(in_array) + + assert(isinstance(out_array, np.ndarray)) + assert(out_array.shape == shape) + assert((out_array == 1).all) + + def test_return_array_size_2d_in(self): + shape = (2, 3) + in_array = np.zeros(shape, dtype=np.float32, order="F") + out_array = m_test.return_array_size_2d_in(in_array) + + assert(isinstance(out_array, np.ndarray)) + assert(out_array.shape == (shape[1],)) + assert((out_array == 1).all) + + def test_return_array_size_2d_out(self): + shape_1 = (2, 3) + shape_2 = (5, 4) + in_array_1 = np.zeros(shape_1, dtype=np.float32, order="F") + in_array_2 = np.zeros(shape_2, dtype=np.float32, order="F") + out_array = m_test.return_array_size_2d_out(in_array_1, in_array_2) + + assert(isinstance(out_array, np.ndarray)) + assert(out_array.shape == (shape_1[0], shape_2[1])) + assert((out_array == 2).all) + + def test_return_derived_type_value(self): + out_value = 1 + value_type = m_test.t_value() + value_type.value = out_value + shape = (2, 3) + size_2d = m_test.t_size_2d() + size_2d.x = shape[0] + size_2d.y = shape[1] + + out_array = m_test.return_derived_type_value(value_type, size_2d) + + assert(isinstance(out_array, np.ndarray)) + assert(out_array.shape == shape) + assert((out_array == out_value).all) + +if __name__ == '__main__': + + unittest.main() diff --git a/f90wrap/pywrapgen.py b/f90wrap/pywrapgen.py index 477d1892..c670abef 100644 --- a/f90wrap/pywrapgen.py +++ b/f90wrap/pywrapgen.py @@ -24,6 +24,7 @@ import os import logging import re +import numpy as np from f90wrap.transform import shorten_long_name from f90wrap import fortran as ft @@ -447,6 +448,62 @@ def visit_Procedure(self, node): ("None if %(arg_py_name)s is None else %(arg_py_name)s._handle") % {"arg_py_name": arg.py_name}, ) + # Add dimension argument for fortran functions that returns an array + if isinstance(node, ft.Function): + + def f902py_name(node, f90_name): + for arg in node.arguments: + if arg.name == f90_name: + return arg.py_name + return "" + + args_py_names = [arg.py_name for arg in node.arguments] + offset = 0 + # Regular arguments are first, compute the index offset + for arg in node.arguments: + offset += len(arg.dims_list()) + for retval in node.ret_val: + the_dim = "" + try: + the_dim = retval.dims_list()[0] + except IndexError: + pass + for dim_str in retval.dims_list(): + # "size" is replaced by "size_bn" ("badname") by numpy.f2py + keyword = "size" + try: + keyword = np.f2py.crackfortran.badnames[keyword] + except KeyError: + pass + match = re.search("%s\((.*)\)" % keyword, dim_str) + + if match: + # Case where return size is size of input + size_arg = match.group(1).split(",") + py_name = f902py_name(node, size_arg[0]) + try: + dim_num = int(size_arg[1]) - 1 + except IndexError: + dim_num = 0 + out_dim = "%s.shape[%d]" % (py_name, dim_num) + else: + # Case where return size is input + py_name = f902py_name(node, dim_str.split("%")[0]) + # It could be a member of an object + members_arg = dim_str.split("%")[1:] + if members_arg: + out_dim = "%s.%s" % (py_name, ".".join(members_arg)) + else: + out_dim = "%s" % (py_name) + + if py_name in args_py_names: + log.info("Adding dimension argument to '%s'" % node.name) + dct["f90_arg_names"] = "%s, %s" % ( + dct["f90_arg_names"], + "n%d=%s" % (offset, out_dim), + ) + offset += 1 + call_line = ( "%(call)s%(mod_name)s.%(subroutine_name)s(%(f90_arg_names)s)" % dct ) diff --git a/f90wrap/transform.py b/f90wrap/transform.py index 9fd086e6..2234a5f5 100644 --- a/f90wrap/transform.py +++ b/f90wrap/transform.py @@ -865,13 +865,14 @@ def visit_Function(self, node): # insert ret_val after last non-optional argument arguments = node.arguments[:] - i = 0 + j = len(arguments) for i, arg in enumerate(arguments): if 'optional' in arg.attributes: + j = i break - arguments.insert(i, node.ret_val) - arguments[i].name = 'ret_' + arguments[i].name - arguments[i].attributes.append('intent(out)') + arguments.insert(j, node.ret_val) + arguments[j].name = 'ret_' + arguments[j].name + arguments[j].attributes.append('intent(out)') new_node = ft.Subroutine(node.name, node.filename, @@ -904,13 +905,27 @@ def visit_Procedure(self, node): ret_val = [] ret_val_doc = None + arguments = [] + + # Push first non-optional arguments + for arg in node.arguments: + if 'optional' in arg.attributes: + break + if 'intent(out)' in arg.attributes: + ret_val.append(arg) + else: + arguments.append(arg) + + # Push Function return value if isinstance(node, ft.Function) and node.ret_val is not None: ret_val.append(node.ret_val) if node.ret_val_doc is not None: ret_val_doc = node.ret_val_doc - arguments = [] + # Push remaining optional arguments for arg in node.arguments: + if not 'optional' in arg.attributes: + continue if 'intent(out)' in arg.attributes: ret_val.append(arg) else: @@ -970,7 +985,8 @@ def visit_Argument(self, node): new_attribs = [] for attrib in node.attributes: if attrib.startswith('dimension('): - new_attribs.append(attrib.replace(old_name, new_name)) + # Only replace if matchs a word + new_attribs.append(re.sub(r'(\b)%s(\b)'%old_name, r'\1%s\2'%new_name, attrib)) else: new_attribs.append(attrib) node.attributes = new_attribs From b4d6039f20d0b83fc73f21b0f25e081444507870 Mon Sep 17 00:00:00 2001 From: daniel Date: Thu, 29 Aug 2024 15:52:30 +0200 Subject: [PATCH 05/12] Change f90wrap generated variable names to enable use of n1, n2 variable names --- examples/CMakeLists.txt | 3 +- examples/Makefile | 3 +- examples/intent_out_size/Makefile | 38 +++++++++++++++++++++++++ examples/intent_out_size/Makefile.meson | 6 ++++ examples/intent_out_size/main.f90 | 26 +++++++++++++++++ examples/intent_out_size/tests.py | 24 ++++++++++++++++ f90wrap/pywrapgen.py | 2 +- f90wrap/transform.py | 10 +++++-- 8 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 examples/intent_out_size/Makefile create mode 100644 examples/intent_out_size/Makefile.meson create mode 100644 examples/intent_out_size/main.f90 create mode 100644 examples/intent_out_size/tests.py diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index d39f1665..58681a75 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -28,6 +28,7 @@ list(APPEND tests kind_map_default docstring return_array + intent_out_size ) foreach(test ${tests}) @@ -38,5 +39,3 @@ foreach(test ${tests}) WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}/${test}" ) endforeach() - - diff --git a/examples/Makefile b/examples/Makefile index 563a92fe..28a8b9b2 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -26,7 +26,8 @@ EXAMPLES = arrayderivedtypes \ return_array \ optional_string \ long_subroutine_name \ - kind_map_default + kind_map_default \ + intent_out_size PYTHON = python diff --git a/examples/intent_out_size/Makefile b/examples/intent_out_size/Makefile new file mode 100644 index 00000000..0285dabd --- /dev/null +++ b/examples/intent_out_size/Makefile @@ -0,0 +1,38 @@ +#======================================================================= +# define the compiler names +#======================================================================= + +CC = gcc +F90 = gfortran +PYTHON = python +CFLAGS = -fPIC +F90FLAGS = -fPIC +PY_MOD = pywrapper +F90_SRC = main.f90 +OBJ = $(F90_SRC:.f90=.o) +F90WRAP_SRC = $(addprefix f90wrap_,${F90_SRC}) +WRAPFLAGS = -v +F2PYFLAGS = --build-dir build +F90WRAP = f90wrap +F2PY = f2py-f90wrap +.PHONY: all clean + +all: test + +clean: + rm -rf *.mod *.smod *.o f90wrap*.f90 ${PY_MOD}.py _${PY_MOD}*.so __pycache__/ .f2py_f2cmap build ${PY_MOD}/ + +main.o: ${F90_SRC} + ${F90} ${F90FLAGS} -c $< -o $@ + +%.o: %.f90 + ${F90} ${F90FLAGS} -c $< -o $@ + +${F90WRAP_SRC}: ${OBJ} + ${F90WRAP} -m ${PY_MOD} ${WRAPFLAGS} ${F90_SRC} + +f2py: ${F90WRAP_SRC} + CFLAGS="${CFLAGS}" ${F2PY} -c -m _${PY_MOD} ${F2PYFLAGS} f90wrap_*.f90 *.o + +test: f2py + ${PYTHON} tests.py diff --git a/examples/intent_out_size/Makefile.meson b/examples/intent_out_size/Makefile.meson new file mode 100644 index 00000000..b2ee9928 --- /dev/null +++ b/examples/intent_out_size/Makefile.meson @@ -0,0 +1,6 @@ +include ../make.meson.inc + +NAME := pywrapper + +test: build + $(PYTHON) tests.py diff --git a/examples/intent_out_size/main.f90 b/examples/intent_out_size/main.f90 new file mode 100644 index 00000000..64ca627e --- /dev/null +++ b/examples/intent_out_size/main.f90 @@ -0,0 +1,26 @@ +module m_intent_out + + implicit none + public + +contains + + subroutine interpolation(n1,n2,a1,a2,output) + ! + integer, intent(in) :: n1,n2 + real,dimension(n1,n2), intent(in) :: a1,a2 + real,dimension(n1,n2), intent(out) :: output + + integer :: i,j + + do j=1,n2 + do i=1,n1 + output(i,j)=(a1(i,j)+a2(i,j))/2 + enddo + enddo + + end subroutine interpolation + +end module m_intent_out + + diff --git a/examples/intent_out_size/tests.py b/examples/intent_out_size/tests.py new file mode 100644 index 00000000..53a9297c --- /dev/null +++ b/examples/intent_out_size/tests.py @@ -0,0 +1,24 @@ +import unittest +import numpy as np + +from pywrapper import m_intent_out + +class TestIntentOut(unittest.TestCase): + + def test_intent_out_size(self): + + a1 = np.array([[1,2], [3,4]], dtype=np.float32, order='F') + a2 = np.array([[2,4], [6,8]], dtype=np.float32, order='F') + output = np.zeros((2,2), dtype=np.float32, order='F') + n1 = 2 + n2 = 2 + + m_intent_out.interpolation(n1,n2,a1,a2,output) + + ref_out = np.array([[1.5,3.], [4.5,6.]], dtype=np.float32, order='F') + + np.testing.assert_array_equal(output, ref_out) + +if __name__ == '__main__': + + unittest.main() diff --git a/f90wrap/pywrapgen.py b/f90wrap/pywrapgen.py index c670abef..d25ec86d 100644 --- a/f90wrap/pywrapgen.py +++ b/f90wrap/pywrapgen.py @@ -500,7 +500,7 @@ def f902py_name(node, f90_name): log.info("Adding dimension argument to '%s'" % node.name) dct["f90_arg_names"] = "%s, %s" % ( dct["f90_arg_names"], - "n%d=%s" % (offset, out_dim), + "f90wrap_n%d=%s" % (offset, out_dim), ) offset += 1 diff --git a/f90wrap/transform.py b/f90wrap/transform.py index 2234a5f5..06c4fad9 100644 --- a/f90wrap/transform.py +++ b/f90wrap/transform.py @@ -580,6 +580,7 @@ class ArrayDimensionConverter(ft.FortranVisitor): def visit_Procedure(self, node): n_dummy = 0 + all_new_dummy_args = [] for arg in node.arguments: dims = [attr for attr in arg.attributes if attr.startswith('dimension')] if dims == []: @@ -601,7 +602,7 @@ def visit_Procedure(self, node): d.replace('len', 'slen'), arg.name)) new_ds.append(d) continue - dummy_arg = ft.Argument(name='n%d' % n_dummy, type='integer', attributes=['intent(hide)']) + dummy_arg = ft.Argument(name='f90wrap_n%d' % n_dummy, type='integer', attributes=['intent(hide)']) if 'intent(out)' not in arg.attributes: dummy_arg.f2py_line = ('!f2py intent(hide), depend(%s) :: %s = shape(%s,%d)' % @@ -614,7 +615,12 @@ def visit_Procedure(self, node): log.debug('adding dummy arguments %r to %s' % (new_dummy_args, node.name)) arg.attributes = ([attr for attr in arg.attributes if not attr.startswith('dimension')] + ['dimension(%s)' % ','.join(new_ds)]) - node.arguments.extend(new_dummy_args) + all_new_dummy_args.extend(new_dummy_args) + + # New dummy args are prepended so that they are defined before being used as array dimensions + # This avoids implicit declaration + if all_new_dummy_args != []: + node.arguments = all_new_dummy_args + node.arguments class MethodFinder(ft.FortranTransformer): From b023d9f108b2294cb8866c0b7d17d51fbcd1c9ff Mon Sep 17 00:00:00 2001 From: daniel Date: Wed, 28 Aug 2024 14:34:47 +0200 Subject: [PATCH 06/12] Add string array input test for f2py to document behaviour and bugs --- examples/CMakeLists.txt | 1 + examples/Makefile | 1 + examples/string_array_input_f2py/Makefile | 34 ++++++++++ .../string_array_input_f2py/Makefile.meson | 6 ++ examples/string_array_input_f2py/main.f90 | 32 ++++++++++ examples/string_array_input_f2py/tests.py | 64 +++++++++++++++++++ 6 files changed, 138 insertions(+) create mode 100644 examples/string_array_input_f2py/Makefile create mode 100644 examples/string_array_input_f2py/Makefile.meson create mode 100644 examples/string_array_input_f2py/main.f90 create mode 100644 examples/string_array_input_f2py/tests.py diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 58681a75..5e91b275 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -29,6 +29,7 @@ list(APPEND tests docstring return_array intent_out_size + string_array_input_f2py ) foreach(test ${tests}) diff --git a/examples/Makefile b/examples/Makefile index 28a8b9b2..989c07d7 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -24,6 +24,7 @@ EXAMPLES = arrayderivedtypes \ type_check \ derivedtypes_procedure \ return_array \ + string_array_input_f2py \ optional_string \ long_subroutine_name \ kind_map_default \ diff --git a/examples/string_array_input_f2py/Makefile b/examples/string_array_input_f2py/Makefile new file mode 100644 index 00000000..d65c23bd --- /dev/null +++ b/examples/string_array_input_f2py/Makefile @@ -0,0 +1,34 @@ +#======================================================================= +# define the compiler names +#======================================================================= + +F90 = gfortran +PYTHON = python3 +CFLAGS = -fPIC +F90FLAGS = -fPIC +PY_MOD = pywrapper +F90_SRC = main.f90 +OBJ = $(F90_SRC:.f90=.o) +SIGNATURES = _signatures.pyf +F2PYFLAGS = --build-dir build +F2PY = f2py +LINK = -lgfortran +.PHONY: all clean + +all: f2py test + +clean: + rm -rf *.mod *.smod *.o f90wrap*.f90 ${PY_MOD}_*.py _${PY_MOD}*.so __pycache__/ .f2py_f2cmap build ${PY_MOD}_*/ ${SIGNATURES} + +%.o: %.f90 + ${F90} ${F90FLAGS} -c $< -o $@ + +${SIGNATURES}: ${F90_SRC} + ${F2PY} ${F90_SRC} -m _${PY_MOD}_sign -h ${SIGNATURES} + +f2py: ${OBJ} ${SIGNATURES} + CFLAGS="${CFLAGS}" ${F2PY} -c -m _${PY_MOD}_sign ${F2PYFLAGS} ${LINK} ${OBJ} ${SIGNATURES} + CFLAGS="${CFLAGS}" ${F2PY} -c -m _${PY_MOD}_no_sign ${F2PYFLAGS} ${F90_SRC} + +test: f2py + ${PYTHON} tests.py diff --git a/examples/string_array_input_f2py/Makefile.meson b/examples/string_array_input_f2py/Makefile.meson new file mode 100644 index 00000000..b2ee9928 --- /dev/null +++ b/examples/string_array_input_f2py/Makefile.meson @@ -0,0 +1,6 @@ +include ../make.meson.inc + +NAME := pywrapper + +test: build + $(PYTHON) tests.py diff --git a/examples/string_array_input_f2py/main.f90 b/examples/string_array_input_f2py/main.f90 new file mode 100644 index 00000000..29c45213 --- /dev/null +++ b/examples/string_array_input_f2py/main.f90 @@ -0,0 +1,32 @@ +subroutine string_in_array(n0, input, output) + implicit none + + integer :: n0 + !f2py intent(hide), depend(input) :: n0 = shape(input,0) + character*(*), intent(in), dimension(n0) :: input + integer, intent(out) :: output + + if(input(1) .eq. "one" .and. input(2) .eq. "two") then + output=0 + else + output=1 + endif +end subroutine string_in_array + +subroutine string_in_array_optional(n0, input, output) + implicit none + + integer :: n0 + !f2py intent(hide), depend(input) :: n0 = shape(input,0) + character*(*), intent(in), optional, dimension(n0) :: input + integer, intent(out) :: output + + output=2 + if(present(input)) then + if(input(1) .eq. "one" .and. input(2) .eq. "two") then + output=0 + else + output=1 + endif + endif +end subroutine string_in_array_optional diff --git a/examples/string_array_input_f2py/tests.py b/examples/string_array_input_f2py/tests.py new file mode 100644 index 00000000..f312d15c --- /dev/null +++ b/examples/string_array_input_f2py/tests.py @@ -0,0 +1,64 @@ +import unittest +import numpy as np +from packaging import version + +import _pywrapper_sign +import _pywrapper_no_sign + +class TestWithSignature(unittest.TestCase): + + @unittest.skipIf(version.parse(np.version.version) < version.parse("1.24.0") , "f2py bug solved https://github.com/numpy/numpy/issues/24706") + def test_string_in_array(self): + in_array = np.array(['one', 'two'], dtype='S3') + output = _pywrapper_sign.string_in_array(in_array) + self.assertEqual(output, 0) + + @unittest.skipIf(version.parse(np.version.version) < version.parse("1.24.0") , "f2py bug solved https://github.com/numpy/numpy/issues/24706") + def test_string_in_array_optional_present(self): + in_array = np.array(['one', 'two'], dtype='S3') + output = _pywrapper_sign.string_in_array_optional(in_array) + self.assertEqual(output, 0) + + def test_string_in_array_optional_not_present(self): + with self.assertRaises((SystemError, ValueError)): + _ = _pywrapper_sign.string_in_array_optional() + +class TestWithoutSignature(unittest.TestCase): + + @unittest.skipIf(version.parse(np.version.version) < version.parse("1.24.0") , "This test is known to fail on numpy version older than 1.24.0, dtype=S# does not work") + def test_string_in_array(self): + in_array = np.array(['one', 'two'], dtype='S3') + output = _pywrapper_no_sign.string_in_array(in_array) + self.assertEqual(output, 0) + + @unittest.skipIf(version.parse(np.version.version) < version.parse("1.24.0") , "This test is known to fail on numpy version older than 1.24.0, dtype=S# does not work") + def test_string_in_array_optional_present(self): + in_array = np.array(['one', 'two'], dtype='S3') + output = _pywrapper_no_sign.string_in_array_optional(in_array) + self.assertEqual(output, 0) + + def test_string_in_array_optional_not_present(self): + with self.assertRaises((SystemError, ValueError)): + _ = _pywrapper_no_sign.string_in_array_optional() + +class TestWithoutSignatureWithCDtype(unittest.TestCase): + + @unittest.skipIf(version.parse(np.version.version) > version.parse("1.23.5") , "This test is known to fail on numpy version newer than 1.23.5, dtype=c should not be used") + def test_string_in_array(self): + in_array = np.array(['one', 'two'], dtype='c') + output = _pywrapper_no_sign.string_in_array(in_array) + self.assertEqual(output, 0) + + @unittest.skipIf(version.parse(np.version.version) > version.parse("1.23.5") , "This test is known to fail on numpy version newer than 1.23.5, dtype=c should not be used") + def test_string_in_array_optional_present(self): + in_array = np.array(['one', 'two'], dtype='c') + output = _pywrapper_no_sign.string_in_array_optional(in_array) + self.assertEqual(output, 0) + + def test_string_in_array_optional_not_present(self): + with self.assertRaises((SystemError, ValueError)): + _ = _pywrapper_no_sign.string_in_array_optional() + +if __name__ == '__main__': + + unittest.main() From 63f3c48568f16b806ddd11c333d344be8a320627 Mon Sep 17 00:00:00 2001 From: daniel Date: Tue, 3 Sep 2024 11:48:57 +0200 Subject: [PATCH 07/12] Update type check feature --- examples/CMakeLists.txt | 1 + examples/type_check/Makefile | 10 +- examples/type_check/kind.map | 4 + examples/type_check/main.f90 | 134 +++++++++- examples/type_check/type_check_test.py | 201 +++++++++++++-- f90wrap/fortran.py | 2 +- f90wrap/pywrapgen.py | 334 ++++++++++++++++--------- f90wrap/transform.py | 9 +- 8 files changed, 541 insertions(+), 154 deletions(-) create mode 100644 examples/type_check/kind.map diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 5e91b275..ab063a08 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -30,6 +30,7 @@ list(APPEND tests return_array intent_out_size string_array_input_f2py + type_check ) foreach(test ${tests}) diff --git a/examples/type_check/Makefile b/examples/type_check/Makefile index ff129a6a..d79ae950 100644 --- a/examples/type_check/Makefile +++ b/examples/type_check/Makefile @@ -11,7 +11,7 @@ PY_MOD = pywrapper F90_SRC = main.f90 OBJ = $(F90_SRC:.f90=.o) F90WRAP_SRC = $(addprefix f90wrap_,${F90_SRC}) -WRAPFLAGS = -v --type-check +WRAPFLAGS = -v --type-check --kind-map kind.map F2PYFLAGS = --build-dir build F90WRAP = f90wrap F2PY = f2py-f90wrap @@ -31,12 +31,8 @@ main.o: ${F90_SRC} ${F90WRAP_SRC}: ${OBJ} ${F90WRAP} -m ${PY_MOD} ${WRAPFLAGS} ${F90_SRC} -f90wrap: ${F90WRAP_SRC} - f2py: ${F90WRAP_SRC} CFLAGS="${CFLAGS}" ${F2PY} -c -m _${PY_MOD} ${F2PYFLAGS} f90wrap_*.f90 *.o -wrapper: f2py - -test: wrapper - python type_check_test.py +test: f2py + ${PYTHON} type_check_test.py diff --git a/examples/type_check/kind.map b/examples/type_check/kind.map new file mode 100644 index 00000000..853b2154 --- /dev/null +++ b/examples/type_check/kind.map @@ -0,0 +1,4 @@ +{\ +'integer':{'1':'signed_char', '2':'short', '4':'int', '8':'long_long'},\ +'real':{'4':'float', '8':'double'},\ +} diff --git a/examples/type_check/main.f90 b/examples/type_check/main.f90 index 35a8f810..4668e979 100644 --- a/examples/type_check/main.f90 +++ b/examples/type_check/main.f90 @@ -1,5 +1,5 @@ -module m_circle +module m_type_test implicit none private @@ -17,17 +17,54 @@ module m_circle end interface is_circle interface write_array + module procedure write_array_int32_0d + module procedure write_array_int64_0d + module procedure write_array_real32_0d + module procedure write_array_real64_0d module procedure write_array_int_1d module procedure write_array_int_2d module procedure write_array_real module procedure write_array_double + module procedure write_array_bool end interface write_array + interface optional_scalar + module procedure optional_scalar_real + module procedure optional_scalar_int + end interface optional_scalar + + interface in_scalar + module procedure in_scalar_int8 + module procedure in_scalar_int16 + module procedure in_scalar_int32 + module procedure in_scalar_int64 + module procedure in_scalar_real32 + module procedure in_scalar_real64 + module procedure in_array_int64 + module procedure in_array_real64 + end interface in_scalar + public :: is_circle public :: write_array public :: is_circle_circle public :: is_circle_square public :: write_array_int_1d + public :: optional_scalar + + public :: write_array_int64_0d + public :: write_array_real64_0d + public :: write_array_real + public :: write_array_double + + public :: in_scalar + public :: in_scalar_int8 + public :: in_scalar_int16 + public :: in_scalar_int32 + public :: in_scalar_int64 + public :: in_scalar_real32 + public :: in_scalar_real64 + public :: in_array_int64 + public :: in_array_real64 contains @@ -43,6 +80,26 @@ subroutine is_circle_square(square, output) output(:) = 0 end subroutine is_circle_square + subroutine write_array_int32_0d(output) + integer(kind=4),intent(inout) :: output + output = 10 + end subroutine write_array_int32_0d + + subroutine write_array_int64_0d(output) + integer(kind=8),intent(inout) :: output + output = 11 + end subroutine write_array_int64_0d + + subroutine write_array_real32_0d(output) + real(kind=4),intent(inout) :: output + output = 12 + end subroutine write_array_real32_0d + + subroutine write_array_real64_0d(output) + real(kind=8),intent(inout) :: output + output = 13 + end subroutine write_array_real64_0d + subroutine write_array_int_1d(output) integer :: output(:) output(:) = 1 @@ -63,6 +120,75 @@ subroutine write_array_double(output) output(:) = 4 end subroutine write_array_double -end module m_circle - - + subroutine write_array_bool(output) + logical :: output(:) + output(:) = .true. + end subroutine write_array_bool + + subroutine optional_scalar_real(output, opt_output) + real, intent(inout) :: output(:) + real, intent(out),optional :: opt_output + output(:) = 10 + if (present(opt_output)) then + opt_output = 20 + endif + end subroutine + + subroutine optional_scalar_int(output, opt_output) + integer, intent(inout) :: output(:) + integer, intent(out),optional :: opt_output + output(:) = 15 + if (present(opt_output)) then + opt_output = 25 + endif + end subroutine + + function in_scalar_int8(input) result(output) + integer(kind=1),intent(in) :: input + integer(kind=4) :: output + output = 108 + end function in_scalar_int8 + + function in_scalar_int16(input) result(output) + integer(kind=2),intent(in) :: input + integer(kind=4) :: output + output = 116 + end function in_scalar_int16 + + function in_scalar_int32(input) result(output) + integer(kind=4),intent(in) :: input + integer(kind=4) :: output + output = 132 + end function in_scalar_int32 + + function in_scalar_int64(input) result(output) + integer(kind=8),intent(in) :: input + integer(kind=4) :: output + output = 164 + end function in_scalar_int64 + + function in_scalar_real32(input) result(output) + real(kind=4),intent(in) :: input + integer(kind=4) :: output + output = 232 + end function in_scalar_real32 + + function in_scalar_real64(input) result(output) + real(kind=8),intent(in) :: input + integer(kind=4) :: output + output = 264 + end function in_scalar_real64 + + function in_array_int64(input) result(output) + integer(kind=8),intent(in) :: input(:) + integer(kind=4) :: output + output = 364 + end function in_array_int64 + + function in_array_real64(input) result(output) + real(kind=8),intent(in) :: input(:) + integer(kind=4) :: output + output = 464 + end function in_array_real64 + +end module m_type_test diff --git a/examples/type_check/type_check_test.py b/examples/type_check/type_check_test.py index cb7d154f..43e377da 100644 --- a/examples/type_check/type_check_test.py +++ b/examples/type_check/type_check_test.py @@ -1,86 +1,241 @@ import unittest import numpy as np +from packaging import version -from pywrapper import m_circle +from pywrapper import m_type_test class TestTypeCheck(unittest.TestCase): def __init__(self, *args, **kwargs): super(TestTypeCheck, self).__init__(*args, **kwargs) - self._circle = m_circle.t_circle() - self._square = m_circle.t_square() + self._circle = m_type_test.t_circle() + self._square = m_type_test.t_square() def test_derived_type_selection(self): out_circle = np.array([-1], dtype=np.int32) + out_string = 'yo' + out_string = 'yo' out_square = np.array([-1], dtype=np.int32) - m_circle.is_circle(self._circle, out_circle) - m_circle.is_circle(self._square, out_square) + m_type_test.is_circle(self._circle, out_circle) + m_type_test.is_circle(self._square, out_square) - assert out_circle[0]==1 - assert out_square[0]==0 + self.assertEqual(out_circle[0], 1) + self.assertEqual(out_square[0], 0) + + def test_shape_selection_0d(self): + out = np.array(-1, dtype=np.int32) + m_type_test.write_array(out) + + self.assertEqual(out, 10) + + def test_kind_selection_int_0d(self): + out = np.array(-1, dtype=np.int64) + m_type_test.write_array(out) + + self.assertEqual(out, 11) + + def test_kind_selection_real32_0d(self): + out = np.array(-1, dtype=np.float32) + m_type_test.write_array(out) + + self.assertEqual(out, 12) + + def test_kind_selection_real64_0d(self): + out = np.array(-1, dtype=np.float64) + m_type_test.write_array(out) + + self.assertEqual(out, 13) def test_shape_selection_1d(self): out = np.array([-1], dtype=np.int32) - m_circle.write_array(out) + m_type_test.write_array(out) - assert out[0]==1 + self.assertEqual(out[0], 1) def test_shape_selection_2d(self): out = np.array([[-1]], dtype=np.int32) - m_circle.write_array(out) + m_type_test.write_array(out) - assert out[0]==2 + self.assertEqual(out[0], 2) def test_type_selection(self): out = np.array([-1], dtype=np.float32) - m_circle.write_array(out) + m_type_test.write_array(out) - assert out[0]==3 + self.assertEqual(out[0], 3) - def test_kind_selection(self): + def test_kind_selection_float_1d(self): out = np.array([-1], dtype=np.float64) - m_circle.write_array(out) + m_type_test.write_array(out) - assert out[0]==4 + self.assertEqual(out[0], 4) + + @unittest.skip("Bool are not supported in interfaces") + def test_kind_selection(self): + out = np.array([False], dtype=np.bool) + m_type_test.write_array(out) + + self.assertEqual(out[0], True) def test_wrong_derived_type(self): out = np.array([-1], dtype=np.int32) with self.assertRaises(TypeError): - m_circle._is_circle_square(self._circle, out) + m_type_test.is_circle_square(self._circle, out) with self.assertRaises(TypeError): - m_circle._is_circle_circle(self._square, out) + m_type_test.is_circle_circle(self._square, out) def test_wrong_kind(self): out = np.array([-1], dtype=np.int64) with self.assertRaises(TypeError): - m_circle._write_array_int_1d(out) + m_type_test.write_array_int_1d(out) def test_wrong_type(self): out = np.array([-1], dtype=np.float32) with self.assertRaises(TypeError): - m_circle._write_array_int_1d(out) + m_type_test.write_array_int_1d(out) def test_wrong_dim(self): out = np.array([[-1]], dtype=np.int32) with self.assertRaises(TypeError): - m_circle._write_array_int_1d(out) + m_type_test.write_array_int_1d(out) def test_no_suitable_version(self): with self.assertRaises(TypeError): - m_circle.is_circle(1., 1.) + m_type_test.is_circle(1., 1.) def test_no_suitable_version_2(self): out = np.array([-1], dtype=np.complex128) with self.assertRaises(TypeError): - m_circle.write_array(out) + m_type_test.write_array(out) + + def test_optional_scalar_real(self): + out = np.array([-1], dtype=np.float32) + opt_out = np.array(-1, dtype=np.float32) + m_type_test.optional_scalar(out) + self.assertEqual(out[0], 10) + self.assertEqual(opt_out, -1) + + m_type_test.optional_scalar(out, opt_out) + self.assertEqual(out[0], 10) + self.assertEqual(opt_out, 20) + def test_optional_scalar_int(self): + out = np.array([-1], dtype=np.int32) + opt_out = np.array(-1, dtype=np.int32) + m_type_test.optional_scalar(out) + self.assertEqual(out[0], 15) + self.assertEqual(opt_out, -1) + + m_type_test.optional_scalar(out, opt_out) + self.assertEqual(out[0], 15) + self.assertEqual(opt_out, 25) + + def test_scalar_out_tolerance_real_32_64(self): + out = np.array(-1, dtype=np.float32) + with self.assertRaises(TypeError) as context: + m_type_test.write_array_real64_0d(out) + + def test_scalar_out_tolerance_int_32_64(self): + out = np.array(-1, dtype=np.int32) + with self.assertRaises(TypeError) as context: + m_type_test.write_array_int64_0d(out) + + def test_array_out_rigid_int_32_64(self): + out = np.array([-1], dtype=np.int32) + with self.assertRaises(TypeError) as context: + m_type_test.write_array_int64_0d(out) + + def test_array_out_rigid_real_32_64(self): + out = np.array([-1], dtype=np.float32) + with self.assertRaises(TypeError) as context: + m_type_test.write_array_double(out) + + def test_scalar_interface_int8(self): + intput = np.array(-1, dtype=np.int8) + out = m_type_test.in_scalar(intput) + self.assertEqual(out, 108) + + def test_scalar_interface_int16(self): + intput = np.array(-1, dtype=np.int16) + out = m_type_test.in_scalar(intput) + self.assertEqual(out, 116) + + def test_scalar_interface_int32(self): + intput = np.array(-1, dtype=np.int32) + out = m_type_test.in_scalar(intput) + self.assertEqual(out, 132) + + def test_scalar_interface_int64(self): + intput = np.array(-1, dtype=np.int64) + out = m_type_test.in_scalar(intput) + self.assertEqual(out, 164) + + def test_scalar_interface_float32(self): + intput = np.array(-1, dtype=np.float32) + out = m_type_test.in_scalar(intput) + self.assertEqual(out, 232) + + def test_scalar_interface_float64(self): + intput = np.array(-1, dtype=np.float64) + out = m_type_test.in_scalar(intput) + self.assertEqual(out, 264) + + def test_array_interface_int64(self): + intput = np.array([-1], dtype=np.int64) + out = m_type_test.in_scalar(intput) + self.assertEqual(out, 364) + + def test_array_interface_float64(self): + intput = np.array([-1], dtype=np.float64) + out = m_type_test.in_scalar(intput) + self.assertEqual(out, 464) + + def test_scalar_in_int8(self): + intput = np.array(-1, dtype=np.int32) + out = m_type_test.in_scalar_int8(intput) + self.assertEqual(out, 108) + + def test_scalar_in_int16(self): + intput = np.array(-1, dtype=np.int32) + out = m_type_test.in_scalar_int16(intput) + self.assertEqual(out, 116) + + def test_scalar_in_int32(self): + intput = np.array(-1, dtype=np.int64) + out = m_type_test.in_scalar_int32(intput) + self.assertEqual(out, 132) + + def test_scalar_in_int64(self): + intput = np.array(-1, dtype=np.int32) + out = m_type_test.in_scalar_int64(intput) + self.assertEqual(out, 164) + + def test_scalar_in_float32(self): + intput = np.array(-1, dtype=np.float64) + out = m_type_test.in_scalar_real32(intput) + self.assertEqual(out, 232) + + def test_scalar_in_float64(self): + intput = np.array(-1, dtype=np.float32) + out = m_type_test.in_scalar_real64(intput) + self.assertEqual(out, 264) + + def test_array_in_int64(self): + intput = np.array([-1], dtype=np.int32) + with self.assertRaises(TypeError) as context: + out = m_type_test.in_array_int64(intput) + + def test_array_in_float64(self): + intput = np.array([-1], dtype=np.float32) + with self.assertRaises(TypeError) as context: + out = m_type_test.in_array_real64(intput) if __name__ == '__main__': diff --git a/f90wrap/fortran.py b/f90wrap/fortran.py index ff6e0bce..79301dbc 100644 --- a/f90wrap/fortran.py +++ b/f90wrap/fortran.py @@ -922,7 +922,7 @@ def normalise_type(typename, kind_map): c_type = f2c_type(typename, kind_map) c_type_to_fortran_kind = { 'char' : '', - 'signed_char' : '', + 'signed_char' : '(1)', 'short' : '(2)', 'int' : '(4)', 'long_long' : '(8)', diff --git a/f90wrap/pywrapgen.py b/f90wrap/pywrapgen.py index d25ec86d..5ef6a50f 100644 --- a/f90wrap/pywrapgen.py +++ b/f90wrap/pywrapgen.py @@ -25,6 +25,7 @@ import logging import re import numpy as np +from packaging import version from f90wrap.transform import shorten_long_name from f90wrap import fortran as ft @@ -130,11 +131,19 @@ def __init__( self.type_check = type_check self.relative = relative + if version.parse(np.version.version) < version.parse("2.0"): + self.numpy_complexwarning = "numpy.ComplexWarning" + else: + self.numpy_complexwarning = "numpy.exceptions.ComplexWarning" + def write_imports(self, insert=0): - default_imports = [(self.f90_mod_name, None), - ('f90wrap.runtime', None), - ('logging', None), - ('numpy', None)] + default_imports = [ + (self.f90_mod_name, None), + ("f90wrap.runtime", None), + ("logging", None), + ("numpy", None), + ("warnings", None), + ] if self.relative: default_imports[0] = ('..', self.f90_mod_name) imp_lines = ['from __future__ import print_function, absolute_import, division'] for (mod, symbol) in default_imports + list(self.imports): @@ -229,6 +238,11 @@ def visit_Module(self, node): index = self.write_imports(index) self.writelines(["_arrays = {}", "_objs = {}", "\n"], insert=index) self.write() + version.parse(np.version.version) > version.parse("1.23.5") + self.write( + f'warnings.filterwarnings("error", category={self.numpy_complexwarning})' + ) + self.write() if self.make_package: self.write( @@ -424,8 +438,26 @@ def visit_Procedure(self, node): dct['subroutine_name'] = shorten_long_name('%(prefix)s%(func_name)s' % dct) if isinstance(node, ft.Function): - dct["result"] = ", ".join([ret_val.name for ret_val in node.ret_val]) - dct["call"] = "%(result)s = " % dct + dct["result"] = ", ".join( + [ret_val.name for ret_val in node.ret_val] + ) + dct["call"] = ", ".join([ret_val.name for ret_val in node.ret_val]) + if dct["call"]: + dct["call"] = dct["call"] + " = " + + py_sign_names = [ + arg.py_name + py_arg_value(arg) for arg in node.arguments + ] + f90_call_names = [ + "%s=%s" % (arg.name, arg.py_value) if arg.py_value else "%s" % arg.name + for arg in node.arguments + ] + + # Add optional argument to specify if function is called from interface + py_sign_names.append("interface_call=False") + + dct["py_arg_names"] = ", ".join(py_sign_names) + dct["f90_arg_names"] = ", ".join(f90_call_names) if ( not self.make_package @@ -534,6 +566,51 @@ def f902py_name(node, f90_name): self.dedent() self.write() + def write_exception_handler(self, dct): + # try to call each in turn until no TypeError raised + self.write("for proc in %(proc_names)s:" % dct) + self.indent() + self.write("exception=None") + self.write("try:") + self.indent() + self.write("return proc(*args, **kwargs, interface_call=True)") + self.dedent() + self.write( + f"except (TypeError, ValueError, AttributeError, IndexError, {self.numpy_complexwarning}) as err:" + ) + self.indent() + self.write("exception = \"'%s: %s'\" % (type(err).__name__, str(err))") + self.write("continue") + self.dedent() + self.dedent() + self.write() + + self.write("argTypes=[]") + self.write("for arg in args:") + self.indent() + self.write("try:") + self.indent() + self.write( + "argTypes.append(\"%s: dims '%s', type '%s',\"\n\" type code '%s'\"\n%(str(type(arg))," + "arg.ndim, arg.dtype, arg.dtype.num))" + ) + self.dedent() + self.write("except AttributeError:") + self.indent() + self.write("argTypes.append(str(type(arg)))") + self.dedent() + self.dedent() + + self.write('raise TypeError("Not able to call a version of "') + self.indent() + self.write('"%(intf_name)s compatible with the provided args:"' % dct) + self.write( + '"\\n%s\\nLast exception was: %s"%("\\n".join(argTypes), exception))' + ) + self.dedent() + self.dedent() + self.write() + def visit_Interface(self, node): log.info("PythonWrapperGenerator visiting interface %s" % node.name) @@ -566,44 +643,7 @@ def visit_Interface(self, node): self.write("def %(intf_name)s(*args, **kwargs):" % dct) self.indent() self.write(self._format_doc_string(node)) - # try to call each in turn until no TypeError raised - self.write("for proc in %(proc_names)s:" % dct) - self.indent() - self.write("try:") - self.indent() - self.write("return proc(*args, **kwargs)") - self.dedent() - self.write("except TypeError:") - self.indent() - self.write("continue") - self.dedent() - self.dedent() - self.write() - - if self.type_check: - self.write("argTypes=[]") - self.write("for arg in args:") - self.indent() - self.write("try:") - self.indent() - self.write( - "argTypes.append(\"%s: dims '%s', type '%s'\"%(str(type(arg))," - "arg.ndim, arg.dtype))" - ) - self.dedent() - self.write("except AttributeError:") - self.indent() - self.write("argTypes.append(str(type(arg)))") - self.dedent() - self.dedent() - - self.write('raise TypeError("Not able to call a version of "') - self.indent() - self.write("\"'%(intf_name)s' compatible with the provided args:\"" % dct) - self.write('"\\n%s\\n"%"\\n".join(argTypes))') - self.dedent() - self.dedent() - self.write() + self.write_exception_handler(dct) def visit_Type(self, node): log.info("PythonWrapperGenerator visiting type %s" % node.name) @@ -937,89 +977,155 @@ def write_type_checks(self, node): # to ensure either the correct version of an interface is used # either an exception is returned for arg in node.arguments: - if "optional" not in arg.attributes: - ft_array_dim_list = list( - filter(lambda x: x.startswith("dimension"), arg.attributes) - ) - if ft_array_dim_list: - if ":" in ft_array_dim_list[0]: - ft_array_dim = ft_array_dim_list[0].count(",") + 1 - else: - ft_array_dim = -1 + # Check if optional argument is being passed + if "optional" in arg.attributes: + self.write("if {0} is not None:".format(arg.py_name)) + self.indent() + + ft_array_dim_list = list( + filter(lambda x: x.startswith("dimension"), arg.attributes) + ) + if ft_array_dim_list: + if ":" in ft_array_dim_list[0]: + ft_array_dim = ft_array_dim_list[0].count(",") + 1 else: - ft_array_dim = 0 + ft_array_dim = -1 + else: + ft_array_dim = 0 - # Checks for derived types - if arg.type.startswith("type") or arg.type.startswith("class"): - cls_mod_name = self.types[ft.strip_type(arg.type)].mod_name - cls_mod_name = self.py_mod_names.get(cls_mod_name, cls_mod_name) + # Checks for derived types + if arg.type.startswith("type") or arg.type.startswith("class"): + cls_mod_name = self.types[ft.strip_type(arg.type)].mod_name + cls_mod_name = self.py_mod_names.get(cls_mod_name, cls_mod_name) - cls_name = normalise_class_name( - ft.strip_type(arg.type), self.class_names + cls_name = normalise_class_name( + ft.strip_type(arg.type), self.class_names + ) + self.write( + "if not isinstance({0}, {1}.{2}) :".format( + arg.py_name, cls_mod_name, cls_name ) + ) + self.indent() + self.write("raise TypeError") + self.dedent() + + if self.make_package: + self.imports.add((self.py_mod_name, cls_mod_name)) + else: + # Checks for Numpy array dimension and types + # It will fail for types that are not in the kind map + # Good enough for now if it works on standrad types + try: + array_type = ft.fortran_array_type(arg.type, self.kind_map) + pytype = ft.f2numpy_type(arg.type, self.kind_map) + except RuntimeError: + continue + + self.write( + "if isinstance({0},(numpy.ndarray, numpy.generic)):".format( + arg.py_name + ) + ) + self.indent() + + convertible_types = [ + np.short, + np.ushort, + np.int32, + np.uintc, + np.int64, + np.uint, + np.longlong, + np.ulonglong, + np.float16, + np.float32, + np.float64, + np.longdouble, + ] + if ft_array_dim == 0 and "intent(in)" in arg.attributes: self.write( - "if not isinstance({0}, {1}.{2}) :".format( - arg.py_name, cls_mod_name, cls_name + "if not interface_call and {0}.dtype.num in {{{1}}}:".format( + arg.py_name, + ", ".join( + [str(atype().dtype.num) for atype in convertible_types] + ), ) ) self.indent() - self.write("raise TypeError") + self.write("{0} = {0}.astype('{1}')".format(arg.py_name, pytype)) self.dedent() - - if self.make_package: - self.imports.add((self.py_mod_name, cls_mod_name)) + if ft_array_dim == -1: + self.write( + "if {0}.dtype.num != {1}:".format(arg.py_name, array_type) + ) + # Allow fortran character to match python ubyte, unicode_ or string_ + elif array_type == np.ubyte().dtype.num: + str_types = { + np.ubyte().dtype.num, + np.bytes_().dtype.num, + np.str_().dtype.num, + } + str_types = {str(num) for num in str_types} + # Python char array have one supplementary dimension + # https://stackoverflow.com/questions/41864984/how-to-pass-array-of-strings-to-fortran-subroutine-using-f2py + if ft_array_dim > 0: + str_dims = {ft_array_dim, ft_array_dim + 1} + else: + str_dims = { + ft_array_dim, + } + str_dims = {str(num) for num in str_dims} + self.write( + "if {0}.ndim not in {{{1}}} or {0}.dtype.num not in {{{2}}}:".format( + arg.py_name, ",".join(str_dims), ",".join(str_types) + ) + ) else: - # Checks for Numpy array dimension and types - # It will fail for types that are not in the kind map - # Good enough for now if it works on standrad types - try: - array_type = ft.fortran_array_type(arg.type, self.kind_map) - except RuntimeError: - continue - - py_type = ft.f2py_type(arg.type) + self.write( + "if {0}.ndim != {1} or {0}.dtype.num != {2}:".format( + arg.py_name, str(ft_array_dim), array_type + ) + ) - # bool are ignored because fortran logical are mapped to integers - if py_type not in ["bool"]: - self.write( - "if isinstance({0},(numpy.ndarray, numpy.generic)):".format( - arg.py_name - ) + self.indent() + self.write( + "raise TypeError(\"Expecting '{0}' (code '{1}')\"\n" + "\" with dim '{2}' but got '%s' (code '%s') with dim '%s'\"\n" + "%({3}.dtype, {3}.dtype.num, {3}.ndim))".format( + ft.f2py_type(arg.type), + array_type, + str(ft_array_dim), + arg.py_name, + ) + ) + self.dedent() + self.dedent() + if ft_array_dim == 0: + self.write( + "elif not isinstance({0},{1}):".format( + arg.py_name, ft.f2py_type(arg.type) ) - self.indent() - if ft_array_dim == -1: - self.write( - "if {0}.dtype.num != {1}:".format( - arg.py_name, array_type - ) - ) - else: - self.write( - "if {0}.ndim != {1} or {0}.dtype.num != {2}:".format( - arg.py_name, str(ft_array_dim), array_type - ) - ) + ) + self.indent() + self.write( + "raise TypeError(\"Expecting '{0}' but got '%s'\"%type({1}))".format( + ft.f2py_type(arg.type), arg.py_name + ) + ) + self.dedent() + else: + self.write("else:") + self.indent() + self.write( + "raise TypeError(\"Expecting numpy array but got '%s'\"%type({0}))".format( + arg.py_name + ) + ) + self.dedent() - self.indent() - self.write("raise TypeError") - self.dedent() - self.dedent() - if ft_array_dim == 0: - # Do not write checks for unknown types - if py_type not in ["unknown"]: - self.write( - "elif not isinstance({0},{1}):".format( - arg.py_name, py_type - ) - ) - self.indent() - self.write("raise TypeError") - self.dedent() - else: - self.write("else:") - self.indent() - self.write("raise TypeError") - self.dedent() + if "optional" in arg.attributes: + self.dedent() def _format_doc_string(self, node): """ diff --git a/f90wrap/transform.py b/f90wrap/transform.py index 06c4fad9..3d394cf3 100644 --- a/f90wrap/transform.py +++ b/f90wrap/transform.py @@ -1031,11 +1031,10 @@ def visit_Argument(self, node): class RenameInterfacesPython(ft.FortranVisitor): def visit_Interface(self, node): for proc in node.procedures: - if hasattr(proc, 'method_name'): - proc.method_name = '_' + proc.method_name - else: - proc.method_name = '_' + proc.name - node.method_name = node.name + if not hasattr(proc, 'method_name'): + proc.method_name = proc.name + if not hasattr(node,'method_name'): + node.method_name = node.name if node.name == 'assignment(=)': node.method_name = 'assignment' elif node.name == 'operator(+)': From ea739d0f083a60231430a6d2a299ea5bd5311318 Mon Sep 17 00:00:00 2001 From: daniel Date: Thu, 29 Aug 2024 14:45:18 +0200 Subject: [PATCH 08/12] Support character(len=*) Fortran syntax and add optional string tests --- examples/CMakeLists.txt | 1 + examples/optional_string/Makefile.meson | 1 + examples/optional_string/main.f90 | 14 +++++++++ examples/optional_string/test.py | 38 +++++++++++++++++++++++++ f90wrap/fortran.py | 8 +++++- 5 files changed, 61 insertions(+), 1 deletion(-) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index ab063a08..b12df103 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -31,6 +31,7 @@ list(APPEND tests intent_out_size string_array_input_f2py type_check + optional_string ) foreach(test ${tests}) diff --git a/examples/optional_string/Makefile.meson b/examples/optional_string/Makefile.meson index 7a9fd602..09da0fb8 100644 --- a/examples/optional_string/Makefile.meson +++ b/examples/optional_string/Makefile.meson @@ -1,6 +1,7 @@ include ../make.meson.inc NAME := pywrapper +WRAPFLAGS += --type-check test: build $(PYTHON) test.py diff --git a/examples/optional_string/main.f90 b/examples/optional_string/main.f90 index 38b28af3..c9a2023f 100644 --- a/examples/optional_string/main.f90 +++ b/examples/optional_string/main.f90 @@ -3,6 +3,7 @@ module m_string_test implicit none private public :: string_in + public :: string_in_array public :: string_to_string public :: string_to_string_array public :: string_out @@ -15,6 +16,19 @@ subroutine string_in(input) character(len=*), intent(in) :: input end subroutine + subroutine string_in_array(input) + character(len=6), intent(in) :: input(:) + integer :: i + + if (input(1).ne."one ") then + call f90wrap_abort("First char input is incorrect, should be 'one', but is '" // input(1) // "'" ) + endif + + if (input(2).ne."two ") then + call f90wrap_abort("Second char input is incorrect, should be 'two', but is '" // input(2) // "'" ) + endif + end subroutine + subroutine string_to_string(input,output) character(len=*), intent(in) :: input character(len=*), intent(out) :: output diff --git a/examples/optional_string/test.py b/examples/optional_string/test.py index e597f79d..40a32013 100644 --- a/examples/optional_string/test.py +++ b/examples/optional_string/test.py @@ -15,6 +15,44 @@ def test_string_in_2(self): def test_string_in_3(self): m_string_test.string_in(np.bytes_('yo')) + def test_string_in_4(self): + with self.assertRaises(TypeError): + m_string_test.string_in(np.array(['yo'])) + + @unittest.skipIf(version.parse(np.version.version) > version.parse("1.23.5") , "This test is known to fail on numpy version newer than 1.23.5, dtype=c should not be used") + def test_string_in_array_deprecated(self): + in_array = np.array(['one ', 'two '], dtype='c') + m_string_test.string_in_array(in_array) + + @unittest.skipIf(version.parse(np.version.version) > version.parse("1.23.5") , "This test is known to fail on numpy version newer than 1.23.5, dtype=c should not be used") + def test_string_in_array_2_deprecated(self): + in_array = np.array(['three ', 'two '], dtype='c') + with self.assertRaises(RuntimeError) as context: + m_string_test.string_in_array(in_array) + + @unittest.skipIf(version.parse(np.version.version) > version.parse("1.23.5") , "This test is known to fail on numpy version newer than 1.23.5, dtype=c should not be used") + def test_string_in_array_3_deprecated(self): + in_array = np.array(['one ', 'four '], dtype='c') + with self.assertRaises(RuntimeError) as context: + m_string_test.string_in_array(in_array) + + @unittest.skipIf(version.parse(np.version.version) < version.parse("1.24.0") , "This test is known to fail on numpy version older than 1.24.0") + def test_string_in_array(self): + in_array = np.array(['one ', 'two '], dtype='S6') + m_string_test.string_in_array(in_array) + + @unittest.skipIf(version.parse(np.version.version) < version.parse("1.24.0") , "This test is known to fail on numpy version older than 1.24.0") + def test_string_in_array_2(self): + in_array = np.array(['three ', 'two '], dtype='S6') + with self.assertRaises(RuntimeError) as context: + m_string_test.string_in_array(in_array) + + @unittest.skipIf(version.parse(np.version.version) < version.parse("1.24.0") , "This test is known to fail on numpy version older than 1.24.0") + def test_string_in_array_3(self): + in_array = np.array(['one ', 'four '], dtype='S6') + with self.assertRaises(RuntimeError) as context: + m_string_test.string_in_array(in_array) + def test_string_to_string(self): in_string = 'yo' out_string = m_string_test.string_to_string(in_string) diff --git a/f90wrap/fortran.py b/f90wrap/fortran.py index 79301dbc..c1c263fe 100644 --- a/f90wrap/fortran.py +++ b/f90wrap/fortran.py @@ -857,8 +857,14 @@ def split_type_kind(typename): type*kind -> (type, kind) type(kind) -> (type, kind) type(kind=kind) -> (type, kind) + character(len=*) -> (character, *) """ - if '*' in typename: + + if typename.startswith('character'): + type = 'character' + kind = typename[len('character'):] + kind = kind.replace('len=', '') + elif '*' in typename: type = typename[:typename.index('*')] kind = typename[typename.index('*') + 1:] elif '(' in typename: From e851595cc2c668ad0a7fe7c98a4b1894a5525c45 Mon Sep 17 00:00:00 2001 From: daniel Date: Wed, 28 Aug 2024 14:37:11 +0200 Subject: [PATCH 09/12] Add long_subroutine_name for cmake --- examples/CMakeLists.txt | 1 + examples/long_subroutine_name/Makefile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index b12df103..0adeac1d 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -32,6 +32,7 @@ list(APPEND tests string_array_input_f2py type_check optional_string + long_subroutine_name ) foreach(test ${tests}) diff --git a/examples/long_subroutine_name/Makefile b/examples/long_subroutine_name/Makefile index 6fe6aa73..c2e87712 100644 --- a/examples/long_subroutine_name/Makefile +++ b/examples/long_subroutine_name/Makefile @@ -11,7 +11,7 @@ PY_MOD = pywrapper F90_SRC = main.f90 OBJ = $(F90_SRC:.f90=.o) F90WRAP_SRC = $(addprefix f90wrap_,${F90_SRC}) -WRAPFLAGS = -v --type-check +WRAPFLAGS = -v F2PYFLAGS = --build-dir build F90WRAP = f90wrap F2PY = f2py-f90wrap From 7da825dbbf4c4422eedb460ed35dbabe96c18eea Mon Sep 17 00:00:00 2001 From: daniel Date: Wed, 28 Aug 2024 14:39:28 +0200 Subject: [PATCH 10/12] Add output_kind test Add support for long type --- examples/CMakeLists.txt | 1 + examples/Makefile | 3 +- examples/output_kind/Makefile | 38 ++++++++++++++++ examples/output_kind/Makefile.meson | 6 +++ examples/output_kind/kind.map | 3 ++ examples/output_kind/main.f90 | 66 ++++++++++++++++++++++++++++ examples/output_kind/test.py | 68 +++++++++++++++++++++++++++++ f90wrap/fortran.py | 2 + 8 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 examples/output_kind/Makefile create mode 100644 examples/output_kind/Makefile.meson create mode 100644 examples/output_kind/kind.map create mode 100644 examples/output_kind/main.f90 create mode 100644 examples/output_kind/test.py diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 0adeac1d..8ffad416 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -33,6 +33,7 @@ list(APPEND tests type_check optional_string long_subroutine_name + output_kind ) foreach(test ${tests}) diff --git a/examples/Makefile b/examples/Makefile index 989c07d7..6f2e1a59 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -28,7 +28,8 @@ EXAMPLES = arrayderivedtypes \ optional_string \ long_subroutine_name \ kind_map_default \ - intent_out_size + intent_out_size \ + output_kind PYTHON = python diff --git a/examples/output_kind/Makefile b/examples/output_kind/Makefile new file mode 100644 index 00000000..01760623 --- /dev/null +++ b/examples/output_kind/Makefile @@ -0,0 +1,38 @@ +#======================================================================= +# define the compiler names +#======================================================================= + +CC = gcc +F90 = gfortran +PYTHON = python +CFLAGS = -fPIC +F90FLAGS = -fPIC +PY_MOD = pywrapper +F90_SRC = main.f90 +OBJ = $(F90_SRC:.f90=.o) +F90WRAP_SRC = $(addprefix f90wrap_,${F90_SRC}) +WRAPFLAGS = -v --type-check --kind-map kind.map +F2PYFLAGS = --build-dir build +F90WRAP = f90wrap +F2PY = f2py-f90wrap +.PHONY: all clean + +all: test + +clean: + rm -rf *.mod *.smod *.o f90wrap*.f90 ${PY_MOD}.py _${PY_MOD}*.so __pycache__/ .f2py_f2cmap build ${PY_MOD}/ + +main.o: ${F90_SRC} + ${F90} ${F90FLAGS} -c $< -o $@ + +%.o: %.f90 + ${F90} ${F90FLAGS} -c $< -o $@ + +${F90WRAP_SRC}: ${OBJ} + ${F90WRAP} -m ${PY_MOD} ${WRAPFLAGS} ${F90_SRC} + +f2py: ${F90WRAP_SRC} + CFLAGS="${CFLAGS}" ${F2PY} -c -m _${PY_MOD} ${F2PYFLAGS} f90wrap_*.f90 *.o + +test: f2py + ${PYTHON} test.py diff --git a/examples/output_kind/Makefile.meson b/examples/output_kind/Makefile.meson new file mode 100644 index 00000000..7a9fd602 --- /dev/null +++ b/examples/output_kind/Makefile.meson @@ -0,0 +1,6 @@ +include ../make.meson.inc + +NAME := pywrapper + +test: build + $(PYTHON) test.py diff --git a/examples/output_kind/kind.map b/examples/output_kind/kind.map new file mode 100644 index 00000000..a3938d63 --- /dev/null +++ b/examples/output_kind/kind.map @@ -0,0 +1,3 @@ +{'integer':{'1':'signed_char', '2':'short', '4':'int', '8':'long'},\ +'real':{'4': 'float', '8': 'double'},\ +} diff --git a/examples/output_kind/main.f90 b/examples/output_kind/main.f90 new file mode 100644 index 00000000..a38cab11 --- /dev/null +++ b/examples/output_kind/main.f90 @@ -0,0 +1,66 @@ + +module m_out_test + implicit none + private + + public :: out_scalar_int1,out_scalar_int2 + public :: out_scalar_int4,out_scalar_int8 + public :: out_scalar_real4,out_scalar_real8 + public :: out_array_int4,out_array_int8 + public :: out_array_real4,out_array_real8 + +contains + + function out_scalar_int1() result(output) + integer(kind=1) :: output + output=1 + end function + + function out_scalar_int2() result(output) + integer(kind=2) :: output + output=2 + end function + + function out_scalar_int4() result(output) + integer(kind=4) :: output + output=4 + end function + + function out_scalar_int8() result(output) + integer(kind=8) :: output + output=8 + end function + + function out_scalar_real4() result(output) + real(kind=4) :: output + output=4 + end function + + function out_scalar_real8() result(output) + real(kind=8) :: output + output=8 + end function + + function out_array_int4() result(output) + integer(kind=4) :: output(1) + output=4 + end function + + function out_array_int8() result(output) + integer(kind=8) :: output(1) + output=8 + end function + + function out_array_real4() result(output) + real(kind=4) :: output(1) + output=4 + end function + + function out_array_real8() result(output) + real(kind=8) :: output(1) + output=8 + end function + +end module m_out_test + + diff --git a/examples/output_kind/test.py b/examples/output_kind/test.py new file mode 100644 index 00000000..89d4bb9b --- /dev/null +++ b/examples/output_kind/test.py @@ -0,0 +1,68 @@ +import unittest +import numpy as np + +from pywrapper import m_out_test + +class TestTypeCheck(unittest.TestCase): + def test_out_scalar_int1(self): + out = m_out_test.out_scalar_int1() + self.assertEqual(1,out) + self.assertIsInstance(out, int) + + def test_out_scalar_int2(self): + out = m_out_test.out_scalar_int2() + self.assertEqual(2,out) + self.assertIsInstance(out, int) + + def test_out_scalar_int4(self): + out = m_out_test.out_scalar_int4() + self.assertEqual(4,out) + self.assertIsInstance(out, int) + + def test_out_scalar_int8(self): + out = m_out_test.out_scalar_int8() + self.assertEqual(8,out) + self.assertIsInstance(out, int) + + def test_out_scalar_real4(self): + out = m_out_test.out_scalar_real4() + self.assertEqual(4.,out) + self.assertIsInstance(out, float) + + def test_out_scalar_real8(self): + out = m_out_test.out_scalar_real8() + self.assertEqual(8.,out) + self.assertIsInstance(out, float) + + def test_out_array_int4(self): + out = m_out_test.out_array_int4() + self.assertEqual(4,out[0]) + self.assertIsInstance(out, np.ndarray) + self.assertIsInstance(out[0], np.intc) + self.assertIsInstance(out[0], np.int32) + + def test_out_array_int8(self): + out = m_out_test.out_array_int8() + self.assertEqual(8,out[0]) + self.assertIsInstance(out, np.ndarray) + self.assertIsInstance(out[0], np.int_) + self.assertIsInstance(out[0], np.int64) + self.assertNotIsInstance(out[0], np.longlong) + + def test_out_array_real4(self): + out = m_out_test.out_array_real4() + self.assertEqual(4.,out[0]) + self.assertIsInstance(out, np.ndarray) + self.assertIsInstance(out[0], np.single) + self.assertIsInstance(out[0], np.float32) + + def test_out_array_real8(self): + out = m_out_test.out_array_real8() + self.assertEqual(8.,out[0]) + self.assertIsInstance(out, np.ndarray) + self.assertIsInstance(out[0], np.double) + self.assertIsInstance(out[0], np.float64) + +if __name__ == '__main__': + + unittest.main() diff --git a/f90wrap/fortran.py b/f90wrap/fortran.py index c1c263fe..e7462ced 100644 --- a/f90wrap/fortran.py +++ b/f90wrap/fortran.py @@ -931,6 +931,7 @@ def normalise_type(typename, kind_map): 'signed_char' : '(1)', 'short' : '(2)', 'int' : '(4)', + 'long' : '(8)', 'long_long' : '(8)', 'float' : '(4)', 'double' : '(8)', @@ -960,6 +961,7 @@ def f2numpy_type(typename, kind_map): 'signed_char' : 'int8', 'short' : 'int16', 'int' : 'int32', + 'long' : 'int64', 'long_long' : 'int64', 'float' : 'float32', 'double' : 'float64', From 954ecbe42f42b21480089e0b913640953fe76d78 Mon Sep 17 00:00:00 2001 From: daniel Date: Wed, 28 Aug 2024 14:40:10 +0200 Subject: [PATCH 11/12] Add remove_pointer_arg test support contiguous --- examples/CMakeLists.txt | 1 + examples/Makefile | 3 +- examples/remove_pointer_arg/Makefile | 38 ++++++++++++++++++++++ examples/remove_pointer_arg/Makefile.meson | 6 ++++ examples/remove_pointer_arg/main.f90 | 27 +++++++++++++++ examples/remove_pointer_arg/tests.py | 19 +++++++++++ f90wrap/parser.py | 4 +-- 7 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 examples/remove_pointer_arg/Makefile create mode 100644 examples/remove_pointer_arg/Makefile.meson create mode 100644 examples/remove_pointer_arg/main.f90 create mode 100644 examples/remove_pointer_arg/tests.py diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 8ffad416..67b4f1c0 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -34,6 +34,7 @@ list(APPEND tests optional_string long_subroutine_name output_kind + remove_pointer_arg ) foreach(test ${tests}) diff --git a/examples/Makefile b/examples/Makefile index 6f2e1a59..eeba9e74 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -29,7 +29,8 @@ EXAMPLES = arrayderivedtypes \ long_subroutine_name \ kind_map_default \ intent_out_size \ - output_kind + output_kind \ + remove_pointer_arg PYTHON = python diff --git a/examples/remove_pointer_arg/Makefile b/examples/remove_pointer_arg/Makefile new file mode 100644 index 00000000..0285dabd --- /dev/null +++ b/examples/remove_pointer_arg/Makefile @@ -0,0 +1,38 @@ +#======================================================================= +# define the compiler names +#======================================================================= + +CC = gcc +F90 = gfortran +PYTHON = python +CFLAGS = -fPIC +F90FLAGS = -fPIC +PY_MOD = pywrapper +F90_SRC = main.f90 +OBJ = $(F90_SRC:.f90=.o) +F90WRAP_SRC = $(addprefix f90wrap_,${F90_SRC}) +WRAPFLAGS = -v +F2PYFLAGS = --build-dir build +F90WRAP = f90wrap +F2PY = f2py-f90wrap +.PHONY: all clean + +all: test + +clean: + rm -rf *.mod *.smod *.o f90wrap*.f90 ${PY_MOD}.py _${PY_MOD}*.so __pycache__/ .f2py_f2cmap build ${PY_MOD}/ + +main.o: ${F90_SRC} + ${F90} ${F90FLAGS} -c $< -o $@ + +%.o: %.f90 + ${F90} ${F90FLAGS} -c $< -o $@ + +${F90WRAP_SRC}: ${OBJ} + ${F90WRAP} -m ${PY_MOD} ${WRAPFLAGS} ${F90_SRC} + +f2py: ${F90WRAP_SRC} + CFLAGS="${CFLAGS}" ${F2PY} -c -m _${PY_MOD} ${F2PYFLAGS} f90wrap_*.f90 *.o + +test: f2py + ${PYTHON} tests.py diff --git a/examples/remove_pointer_arg/Makefile.meson b/examples/remove_pointer_arg/Makefile.meson new file mode 100644 index 00000000..b2ee9928 --- /dev/null +++ b/examples/remove_pointer_arg/Makefile.meson @@ -0,0 +1,6 @@ +include ../make.meson.inc + +NAME := pywrapper + +test: build + $(PYTHON) tests.py diff --git a/examples/remove_pointer_arg/main.f90 b/examples/remove_pointer_arg/main.f90 new file mode 100644 index 00000000..ed0dc800 --- /dev/null +++ b/examples/remove_pointer_arg/main.f90 @@ -0,0 +1,27 @@ +program main + +end program + +module m_test + + implicit none + public + +contains + + function to_be_ignored_1() result(ptr) + real,pointer :: ptr(:,:) + ptr => null() + end function to_be_ignored_1 + + function to_be_ignored_2() result(ptr) + real,pointer,contiguous :: ptr(:,:) + ptr => null() + end function to_be_ignored_2 + + function not_to_be_ignored() result(out_int) + integer :: out_int + out_int = 1 + end function not_to_be_ignored + +end module m_test diff --git a/examples/remove_pointer_arg/tests.py b/examples/remove_pointer_arg/tests.py new file mode 100644 index 00000000..b8340be8 --- /dev/null +++ b/examples/remove_pointer_arg/tests.py @@ -0,0 +1,19 @@ +import unittest + +from pywrapper import m_test + +class TestReturnArray(unittest.TestCase): + + def not_ignored(self): + _ = m_test.not_to_be_ignored() + + def ignored_1(self): + with self.assertRaises(AttributeError): + _ = m_test.to_be_ignored_1() + + def ignored_2(self): + with self.assertRaises(AttributeError): + _ = m_test.to_be_ignored_2() + +if __name__ == '__main__': + unittest.main() diff --git a/f90wrap/parser.py b/f90wrap/parser.py index 8009f2b4..592034a1 100644 --- a/f90wrap/parser.py +++ b/f90wrap/parser.py @@ -67,7 +67,7 @@ program = re.compile('^program', re.IGNORECASE) program_end = re.compile('^end\s*program|end$', re.IGNORECASE) -attribs = r'allocatable|pointer|save|dimension *\(.*?\)|parameter|target|public|private|extends *\(.*?\)' # jrk33 added target +attribs = r'allocatable|pointer|save|contiguous|dimension *\(.*?\)|parameter|target|public|private|extends *\(.*?\)' # jrk33 added target type_re = re.compile(r'^type((,\s*(' + attribs + r')\s*)*)(::)?\s*(?!\()', re.IGNORECASE) type_end = re.compile('^end\s*type|end$', re.IGNORECASE) @@ -76,7 +76,7 @@ prefixes = r'elemental|impure|module|non_recursive|pure|recursive' types = r'double precision|(real\s*(\(.*?\))?)|(complex\s*(\(.*?\))?)|(integer\s*(\(.*?\))?)|(logical)|(character\s*(\(.*?\))?)|(type\s*\().*?(\))|(class\s*\().*?(\))' -a_attribs = r'allocatable|pointer|save|dimension\(.*?\)|intent\(.*?\)|optional|target|public|private' +a_attribs = r'allocatable|pointer|save|dimension\(.*?\)|intent\(.*?\)|optional|target|public|private|contiguous' types_re = re.compile(types, re.IGNORECASE) From 4f8d45076c8353b916803dc32ef4d920f5139840 Mon Sep 17 00:00:00 2001 From: daniel Date: Wed, 28 Aug 2024 15:51:35 +0200 Subject: [PATCH 12/12] Handle several interfaces referencing same procedures --- f90wrap/f90wrapgen.py | 7 +++++++ f90wrap/transform.py | 32 ++++++++------------------------ 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/f90wrap/f90wrapgen.py b/f90wrap/f90wrapgen.py index 4a56dc34..97369114 100644 --- a/f90wrap/f90wrapgen.py +++ b/f90wrap/f90wrapgen.py @@ -95,6 +95,7 @@ def __init__( self.kind_map = kind_map self.types = types self.default_to_inout = default_to_inout + self.routines = [] def visit_Root(self, node): """ @@ -465,6 +466,12 @@ def visit_Procedure(self, node): call_name = node.orig_name if hasattr(node, "call_name"): call_name = node.call_name + + if node.name in self.routines: + return self.generic_visit(node) + + self.routines.append(node.name) + log.info( "F90WrapperGenerator visiting routine %s call_name %s mod_name %r" % (node.name, call_name, node.mod_name) diff --git a/f90wrap/transform.py b/f90wrap/transform.py index 3d394cf3..98c5f925 100644 --- a/f90wrap/transform.py +++ b/f90wrap/transform.py @@ -1131,22 +1131,6 @@ def visit_Module(self, node): # original resolution logic was implemented when resolution occurred in # the parser. Technically this is quadratic complexity, but the number # of interface prototypes is generally small... - def inject_procedure(interfaces, procedure): - for iface in interfaces: - for i, p in enumerate(iface.procedures): - if procedure.name == p.name: - log.debug("Procedure %s moved to interface %s", procedure.name, iface.name) - iface.procedures[i] = procedure # Replace the prototype - return True - log.debug(f"Procedure %s is not used in any interface", procedure.name) - return False - - unused = [] - for mp in node.procedures: - if not inject_procedure(node.interfaces, mp): - unused.append(mp) - node.procedures = unused - return node # Attempt 1: # Insert procedures at first reference. Elegant and equivalent to Option 0, @@ -1163,14 +1147,14 @@ def inject_procedure(interfaces, procedure): # fortran code gen b/c identically named wrappers will be generated for # each interface, causing a name clash. This could be fixed in code gen # by adding the interface name to the wrapper function name. - #procedure_map = { p.name:p for p in node.procedures } - #unused = set(procedure_map.keys()) - #for int in node.interfaces: - # iprocs = { p.name for p in int.procedures } - # unused -= iprocs # Can't eagerly remove b/c may be in multiple interfaces - # int.procedures = [ procedure_map[p] for p in iprocs ] - #node.procedures = [ procedure_map[p] for p in unused ] - #return node + procedure_map = { p.name:p for p in node.procedures } + unused = set(procedure_map.keys()) + for int in node.interfaces: + iprocs = { p.name for p in int.procedures } + unused -= iprocs # Can't eagerly remove b/c may be in multiple interfaces + int.procedures = [ procedure_map[p] for p in iprocs ] + node.procedures = [ procedure_map[p] for p in unused ] + return node class ResolveBindingPrototypes(ft.FortranTransformer):