From c1a4c3cede86433d698a5dcf4ed967e106b29d8b Mon Sep 17 00:00:00 2001 From: Christopher Albert Date: Thu, 14 Nov 2024 19:33:46 +0000 Subject: [PATCH] Abstract classes --- .../issue206_subroutine_oldstyle/Makefile | 3 +- .../issue235_allocatable_classes/Makefile | 2 +- examples/issue41_abstract_classes/Makefile | 26 ++ .../issue41_abstract_classes/Makefile.meson | 6 + examples/issue41_abstract_classes/main.f90 | 24 ++ .../issue41_abstract_classes/myclass_base.f90 | 17 ++ .../myclass_factory.f90 | 26 ++ .../issue41_abstract_classes/myclass_impl.f90 | 27 ++ .../myclass_impl2.f90 | 27 ++ examples/issue41_abstract_classes/run.py | 19 ++ .../test_parser_abstract_iface.py | 50 ++++ f90wrap/f90wrapgen.py | 33 +-- f90wrap/parser.py | 254 ++++++++++++------ f90wrap/pywrapgen.py | 177 ++++++------ f90wrap/transform.py | 20 +- 15 files changed, 506 insertions(+), 205 deletions(-) create mode 100644 examples/issue41_abstract_classes/Makefile create mode 100644 examples/issue41_abstract_classes/Makefile.meson create mode 100644 examples/issue41_abstract_classes/main.f90 create mode 100644 examples/issue41_abstract_classes/myclass_base.f90 create mode 100644 examples/issue41_abstract_classes/myclass_factory.f90 create mode 100644 examples/issue41_abstract_classes/myclass_impl.f90 create mode 100644 examples/issue41_abstract_classes/myclass_impl2.f90 create mode 100644 examples/issue41_abstract_classes/run.py create mode 100644 examples/issue41_abstract_classes/test_parser_abstract_iface.py diff --git a/examples/issue206_subroutine_oldstyle/Makefile b/examples/issue206_subroutine_oldstyle/Makefile index 1d5c4693..a67587ab 100644 --- a/examples/issue206_subroutine_oldstyle/Makefile +++ b/examples/issue206_subroutine_oldstyle/Makefile @@ -8,8 +8,7 @@ PYTHON = python all: subroutine_oldstyle.o f90wrap -m itest -P subroutine_oldstyle.f -v - f2py-f90wrap --fcompiler=$(FC) --build-dir . -c -m _itest f90wrap_toplevel.f90 \ - subroutine_oldstyle.o + f2py-f90wrap --build-dir . -c -m _itest f90wrap_toplevel.f90 subroutine_oldstyle.o test: all $(PYTHON) run.py diff --git a/examples/issue235_allocatable_classes/Makefile b/examples/issue235_allocatable_classes/Makefile index b6d5e175..b00ced10 100644 --- a/examples/issue235_allocatable_classes/Makefile +++ b/examples/issue235_allocatable_classes/Makefile @@ -2,7 +2,7 @@ FC = gfortran FCFLAGS = -fPIC PYTHON = python -all: wrapper +all: test test: wrapper $(PYTHON) run.py diff --git a/examples/issue41_abstract_classes/Makefile b/examples/issue41_abstract_classes/Makefile new file mode 100644 index 00000000..b9fafe5c --- /dev/null +++ b/examples/issue41_abstract_classes/Makefile @@ -0,0 +1,26 @@ +FC = gfortran +FCFLAGS = -fPIC +PYTHON = python + +all: test test_abstract_classes.x + +test: wrapper + $(PYTHON) run.py + +test_abstract_classes.x: main.f90 myclass_base.o myclass_impl.o myclass_impl2.o myclass_factory.o + $(FC) $(FCFLAGS) -o $@ $^ + +wrapper: myclass_base.o myclass_impl.o myclass_impl2.o myclass_factory.o + f90wrap -m itest -P myclass_base.f90 myclass_impl.f90 myclass_impl2.f90 myclass_factory.f90 -v + f2py-f90wrap --build-dir . -c -m _itest --opt="-O0 -g" \ + f90wrap_myclass_base.f90 f90wrap_myclass_impl.f90 f90wrap_myclass_impl2.f90 f90wrap_myclass_factory.f90 \ + myclass_base.o myclass_impl.o myclass_impl2.o myclass_factory.o + +%.o : %.f90 + $(FC) $(FCFLAGS) -c -g -O0 $< -o $@ + +clean: + rm -f *.o f90wrap*.f90 *.so *.mod *.x + rm -rf src.*/ + rm -rf itest/ + -rm -rf src.*/ .f2py_f2cmap .libs/ __pycache__/ diff --git a/examples/issue41_abstract_classes/Makefile.meson b/examples/issue41_abstract_classes/Makefile.meson new file mode 100644 index 00000000..1fe6c182 --- /dev/null +++ b/examples/issue41_abstract_classes/Makefile.meson @@ -0,0 +1,6 @@ +include ../make.meson.inc + +NAME := itest + +test: build + $(PYTHON) run.py diff --git a/examples/issue41_abstract_classes/main.f90 b/examples/issue41_abstract_classes/main.f90 new file mode 100644 index 00000000..1713b4de --- /dev/null +++ b/examples/issue41_abstract_classes/main.f90 @@ -0,0 +1,24 @@ +program main + use myclass_factory, only: create_myclass + use myclass_base, only: myclass_t + implicit none + + print *, "Start" + + call test() + + print *, "Done" + +contains + +subroutine test + real :: x + class(myclass_t), allocatable :: myobject + + myobject = create_myclass("impl") + call myobject%get_value(x) + + print *, "Value: ", x +end subroutine test + +end program main diff --git a/examples/issue41_abstract_classes/myclass_base.f90 b/examples/issue41_abstract_classes/myclass_base.f90 new file mode 100644 index 00000000..e8929688 --- /dev/null +++ b/examples/issue41_abstract_classes/myclass_base.f90 @@ -0,0 +1,17 @@ +module myclass_base +implicit none + +type, abstract :: myclass_t +contains + procedure(get_value_i), deferred :: get_value +end type myclass_t + +abstract interface + subroutine get_value_i(self, value) + import myclass_t + class(myclass_t), intent(in) :: self + real, intent(out) :: value + end subroutine get_value_i +end interface + +end module myclass_base diff --git a/examples/issue41_abstract_classes/myclass_factory.f90 b/examples/issue41_abstract_classes/myclass_factory.f90 new file mode 100644 index 00000000..7c04bf2e --- /dev/null +++ b/examples/issue41_abstract_classes/myclass_factory.f90 @@ -0,0 +1,26 @@ +module myclass_factory + +use myclass_base, only: myclass_t +use myclass_impl, only: myclass_impl_t +use myclass_impl2, only: myclass_impl2_t +implicit none + +contains + +function create_myclass(impl_type) result(myobject) + class(myclass_t), allocatable :: myobject + + character(*), intent(in) :: impl_type + + select case(impl_type) + case("impl") + allocate(myclass_impl_t :: myobject) + case("impl2") + allocate(myclass_impl2_t :: myobject) + case default + print *, "create_field_can: Unknown implementation: ", impl_type + error stop + end select +end function create_myclass + +end module myclass_factory diff --git a/examples/issue41_abstract_classes/myclass_impl.f90 b/examples/issue41_abstract_classes/myclass_impl.f90 new file mode 100644 index 00000000..e5fea8f9 --- /dev/null +++ b/examples/issue41_abstract_classes/myclass_impl.f90 @@ -0,0 +1,27 @@ +module myclass_impl + +use myclass_base, only: myclass_t +implicit none + +type, extends(myclass_t) :: myclass_impl_t +contains + procedure :: get_value => get_value_impl + final :: myclass_impl_destroy +end type myclass_impl_t + +contains + +subroutine get_value_impl(self, value) + class(myclass_impl_t), intent(in) :: self + real, intent(out) :: value + + value = 1.0 +end subroutine get_value_impl + +subroutine myclass_impl_destroy(self) + type(myclass_impl_t), intent(inout) :: self + + print *, "Finalising myclass_impl_t" +end subroutine myclass_impl_destroy + +end module myclass_impl diff --git a/examples/issue41_abstract_classes/myclass_impl2.f90 b/examples/issue41_abstract_classes/myclass_impl2.f90 new file mode 100644 index 00000000..6f81151c --- /dev/null +++ b/examples/issue41_abstract_classes/myclass_impl2.f90 @@ -0,0 +1,27 @@ +module myclass_impl2 + +use myclass_base, only: myclass_t +implicit none + +type, extends(myclass_t) :: myclass_impl2_t +contains + procedure :: get_value => get_value_impl2 + final :: myclass_impl2_destroy +end type myclass_impl2_t + +contains + +subroutine get_value_impl2(self, value) + class(myclass_impl2_t), intent(in) :: self + real, intent(out) :: value + + value = 2.0 +end subroutine get_value_impl2 + +subroutine myclass_impl2_destroy(self) + type(myclass_impl2_t), intent(inout) :: self + + print *, "Finalising myclass_impl2_t" +end subroutine myclass_impl2_destroy + +end module myclass_impl2 diff --git a/examples/issue41_abstract_classes/run.py b/examples/issue41_abstract_classes/run.py new file mode 100644 index 00000000..219315fc --- /dev/null +++ b/examples/issue41_abstract_classes/run.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +import itest + +REF = 1.0 +TOL = 1.0e-6 + +obj = itest.myclass_factory.create_myclass("impl") + +output = obj.get_value() +assert(abs(output-REF)') - name = name.strip().lower() - log.debug('found generic binding %s => %s', name, targets) - out.append(Binding( - name=name, + if m == None: + return check_deferred_binding(cl, file) + + type = m.group(1).strip().lower() + attrs = m.group(4) + bindings = m.group(6) + if attrs: + attrs = [a.strip().lower() for a in attrs.split(',')] + out = [] + if type == 'generic': + name, targets = bindings.split('=>') + name = name.strip().lower() + log.debug('found generic binding %s => %s', name, targets) + out.append(Binding( + name=name, + lineno=file.lineno, + filename=file.filename, + type=type, + attributes=attrs, + procedures=[ + Prototype( + name=t.strip().lower(), lineno=file.lineno, - filename=file.filename, - type=type, - attributes=attrs, - procedures=[ - Prototype( - name=t.strip().lower(), - lineno=file.lineno, - filename=file.filename - ) - for t in targets.split(',') - ], - )) - else: - for b in bindings.split(','): - name, *target = [ word.strip().lower() for word in b.split('=>')] - name = name.strip().lower() - target = target[0] if target else name - log.debug('found %s binding %s => %s', type, name, target) - out.append(Binding( - name=name, + filename=file.filename + ) + for t in targets.split(',') + ], + )) + else: + for b in bindings.split(','): + name, *target = [ word.strip().lower() for word in b.split('=>')] + name = name.strip().lower() + target = target[0] if target else name + log.debug('found %s binding %s => %s', type, name, target) + out.append(Binding( + name=name, + lineno=file.lineno, + filename=file.filename, + type=type, + attributes=attrs, + procedures=[ + Prototype( + name=target.strip().lower(), lineno=file.lineno, filename=file.filename, - type=type, - attributes=attrs, - procedures=[ - Prototype( - name=target.strip().lower(), - lineno=file.lineno, - filename=file.filename, - ), - ], - )) - cl = file.next() - return [out, cl] - else: + ), + ], + )) + cl = file.next() + return [out, cl] + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +def check_deferred_binding(cl, file): + m = deferred_binding.match(cl) + if m == None: return [None, cl] + type = m.group(1).strip().lower() + interface = m.group(2).strip().lower() + attrs = m.group(5) + name = m.group(7) + if attrs: + attrs = [a.strip().lower() for a in attrs.split(',')] + out = [] + log.debug('found deferred %s(%s) binding %s', type, interface, name) + out.append(Binding( + name=name, + lineno=file.lineno, + filename=file.filename, + type=type, + attributes=attrs, + procedures=[ + Prototype( + name=interface, + lineno=file.lineno, + filename=file.filename + ), + ], + )) + cl = file.next() + return [out, cl] # +++++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/f90wrap/pywrapgen.py b/f90wrap/pywrapgen.py index ddc107c5..c4af838b 100644 --- a/f90wrap/pywrapgen.py +++ b/f90wrap/pywrapgen.py @@ -295,64 +295,65 @@ def write_constructor(self, node): self.write('raise(NotImplementedError("This is an abstract class"))') self.dedent() self.write() - else: - handle_arg = ft.Argument( - name="handle", - filename=node.filename, - doc=["Opaque reference to existing derived type instance"], - lineno=node.lineno, - attributes=["intent(in)", "optional"], - type="integer", - ) - handle_arg.py_name = "handle" + return - # special case for constructors: return value is 'self' argument, - # plus we add an extra optional argument - args = node.arguments + [handle_arg] + handle_arg = ft.Argument( + name="handle", + filename=node.filename, + doc=["Opaque reference to existing derived type instance"], + lineno=node.lineno, + attributes=["intent(in)", "optional"], + type="integer", + ) + handle_arg.py_name = "handle" - dct = dict( - func_name=node.name, - prefix=self.prefix, - mod_name=self.f90_mod_name, - py_arg_names=", ".join( - [ - "%s%s" - % (arg.py_name, "optional" in arg.attributes and "=None" or "") - for arg in args - ] - ), - f90_arg_names=", ".join( - ["%s=%s" % (arg.name, arg.py_value) for arg in node.arguments] - ), - ) + # special case for constructors: return value is 'self' argument, + # plus we add an extra optional argument + args = node.arguments + [handle_arg] - if node.mod_name is not None: - dct["func_name"] = node.mod_name + "__" + node.name - dct["subroutine_name"] = shorten_long_name("%(prefix)s%(func_name)s" % dct) + dct = dict( + func_name=node.name, + prefix=self.prefix, + mod_name=self.f90_mod_name, + py_arg_names=", ".join( + [ + "%s%s" + % (arg.py_name, "optional" in arg.attributes and "=None" or "") + for arg in args + ] + ), + f90_arg_names=", ".join( + ["%s=%s" % (arg.name, arg.py_value) for arg in node.arguments] + ), + ) - self.write("def __init__(self, %(py_arg_names)s):" % dct) - self.indent() - 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( - arg.py_value, - ( - "(None if %(arg_py_name)s is None else %(" - "arg_py_name)s._handle)" - ) - % {"arg_py_name": arg.py_name}, + if node.mod_name is not None: + dct["func_name"] = node.mod_name + "__" + node.name + dct["subroutine_name"] = shorten_long_name("%(prefix)s%(func_name)s" % dct) + + self.write("def __init__(self, %(py_arg_names)s):" % dct) + self.indent() + 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( + arg.py_value, + ( + "(None if %(arg_py_name)s is None else %(" + "arg_py_name)s._handle)" ) - self.write("f90wrap.runtime.FortranDerivedType.__init__(self)") + % {"arg_py_name": arg.py_name}, + ) + self.write("f90wrap.runtime.FortranDerivedType.__init__(self)") - self.write( - "result = %(mod_name)s.%(subroutine_name)s(%(f90_arg_names)s)" % dct - ) - self.write( - "self._handle = result[0] if isinstance(result, tuple) else result" - ) - self.dedent() - self.write() + self.write( + "result = %(mod_name)s.%(subroutine_name)s(%(f90_arg_names)s)" % dct + ) + self.write( + "self._handle = result[0] if isinstance(result, tuple) else result" + ) + self.dedent() + self.write() def write_classmethod(self, node): dct = dict( @@ -415,51 +416,48 @@ def write_classmethod(self, node): def write_destructor(self, node): if "abstract" in node.attributes: - self.write("def __del__(self):") - self.indent() - self.write('raise(NotImplementedError("This is an abstract class"))') - self.dedent() - self.write() - else: - dct = dict( - func_name=node.name, - prefix=self.prefix, - mod_name=self.f90_mod_name, - py_arg_names=", ".join( - [ - "%s%s" - % (arg.py_name, "optional" in arg.attributes and "=None" or "") - for arg in node.arguments - ] - ), - f90_arg_names=", ".join( - ["%s=%s" % (arg.name, arg.py_value) for arg in node.arguments] - ), - ) - if node.mod_name is not None: - dct["func_name"] = node.mod_name + "__" + node.name - dct["subroutine_name"] = shorten_long_name("%(prefix)s%(func_name)s" % dct) + return - self.write("def __del__(%(py_arg_names)s):" % dct) - self.indent() - 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) - self.dedent() - self.dedent() - self.write() + dct = dict( + func_name=node.name, + prefix=self.prefix, + mod_name=self.f90_mod_name, + py_arg_names=", ".join( + [ + "%s%s" + % (arg.py_name, "optional" in arg.attributes and "=None" or "") + for arg in node.arguments + ] + ), + f90_arg_names=", ".join( + ["%s=%s" % (arg.name, arg.py_value) for arg in node.arguments] + ), + ) + if node.mod_name is not None: + dct["func_name"] = node.mod_name + "__" + node.name + dct["subroutine_name"] = shorten_long_name("%(prefix)s%(func_name)s" % dct) + + self.write("def __del__(%(py_arg_names)s):" % dct) + self.indent() + 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) + self.dedent() + self.dedent() + self.write() def visit_Procedure(self, node): log.info("PythonWrapperGenerator visiting routine %s" % node.name) - if "classmethod" in node.attributes: - self.write_classmethod(node) - elif "constructor" in node.attributes: + if "constructor" in node.attributes: self.write_constructor(node) elif "destructor" in node.attributes: self.write_destructor(node) - elif "abstract" in node.attributes: + elif "classmethod" in node.attributes: + self.write_classmethod(node) + elif "abstract" in node.attributes and not "method" in node.attributes: return self.generic_visit(node) + else: dct = dict( func_name=node.name, @@ -509,6 +507,7 @@ def visit_Procedure(self, node): ): # procedures outside of derived types become static methods self.write("@staticmethod") + self.write("def %(method_name)s(%(py_arg_names)s):" % dct) self.indent() self.write(self._format_doc_string(node)) diff --git a/f90wrap/transform.py b/f90wrap/transform.py index d7f966ba..32f388a9 100644 --- a/f90wrap/transform.py +++ b/f90wrap/transform.py @@ -1284,6 +1284,7 @@ class ResolveBindingPrototypes(ft.FortranTransformer): """ def visit_Module(self, node): procedure_map = { p.name:p for p in node.procedures } + proc_interf_map = procedures_and_abstract_interfaces(procedure_map, node) for type in node.types: # Pass 1: Associate module procedures with specific bindings @@ -1291,7 +1292,7 @@ def visit_Module(self, node): if binding.type == 'generic': continue proto = binding.procedures[0] # Only generics have multiple procedures - proc = procedure_map[proto.name] + proc = proc_interf_map[proto.name] proc = copy.deepcopy(proc) log.debug('Creating method for %s from procedure %s.', type.name, proc.name) proc.type_name = type.name @@ -1310,6 +1311,23 @@ def visit_Module(self, node): return node +def procedures_and_abstract_interfaces(procedure_map, node): + """ + Add procedures from abstract interfaces to the procedure map + + This is needed because abstract interfaces are not included in the module + interfaces list, and so are not resolved by ResolveInterfacePrototypes. + """ + extended_procedure_map = procedure_map.copy() + for int in node.interfaces: + if not 'abstract' in int.attributes: + continue + for proc in int.procedures: + if proc.name not in extended_procedure_map: + extended_procedure_map[proc.name] = proc + return extended_procedure_map + + class BindConstructorInterfaces(ft.FortranTransformer): """ Moves interfaces named after a type into that type and marks as constructor.