diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index 4883822d..0e3d72a6 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -52,6 +52,7 @@ jobs: uses: pypa/cibuildwheel@v2.21.3 env: CIBW_BUILD: ${{ matrix.python }}-${{ matrix.buildplat[1] }}* + CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 CIBW_ARCHS: ${{ matrix.buildplat[2] }} CIBW_ENVIRONMENT_MACOS: MACOSX_DEPLOYMENT_TARGET=13.0 CIBW_ENVIRONMENT_PASS_LINUX: RUNNER_OS diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index f11532f6..02bb25ea 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -40,6 +40,7 @@ list(APPEND tests issue306_allocatable_realloc issue307_logical_array issue32 + issue373_optional_constructor issue41_abstract_classes keep_single_interface keyword_renaming_issue160 diff --git a/examples/Makefile b/examples/Makefile index 1f116c8b..a2373099 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -41,6 +41,7 @@ EXAMPLES = \ issue32 \ issue353_selected_kind \ issue357_name_conflict \ + issue373_optional_constructor \ issue41_abstract_classes \ keep_single_interface \ keyword_renaming_issue160 \ diff --git a/examples/issue373_optional_constructor/Makefile b/examples/issue373_optional_constructor/Makefile new file mode 100644 index 00000000..88ce63ea --- /dev/null +++ b/examples/issue373_optional_constructor/Makefile @@ -0,0 +1,33 @@ +#======================================================================= +# define the compiler names +#======================================================================= + +include ../make.inc +PY_MOD = pywrapper +F90_SRC = main.f90 +OBJ = $(F90_SRC:.f90=.o) +F90WRAP_SRC = $(addprefix f90wrap_,${F90_SRC}) +WRAPFLAGS = -v +F2PY = f2py-f90wrap +SRC_AR = libsrc.a +.PHONY: all clean + +all: test + +clean: + rm -rf *.mod *.smod *.o ${SRC_AR} f90wrap*.f90 ${PY_MOD}.py _${PY_MOD}*.so __pycache__/ .f2py_f2cmap build ${PY_MOD}/ + +%.o: %.f90 + ${F90} ${F90FLAGS} -c $< -o $@ + +${F90WRAP_SRC}: ${OBJ} + ${F90WRAP} -m ${PY_MOD} ${WRAPFLAGS} ${F90_SRC} + +${SRC_AR}: ${OBJ} + ar rcs $@ ${OBJ} + +f2py: ${F90WRAP_SRC} ${SRC_AR} + CFLAGS="${CFLAGS}" ${F2PY} -c -m _${PY_MOD} ${F2PYFLAGS} f90wrap_*.f90 -L. -lsrc + +test: f2py + ${PYTHON} tests.py diff --git a/examples/issue373_optional_constructor/Makefile.meson b/examples/issue373_optional_constructor/Makefile.meson new file mode 100644 index 00000000..b2ee9928 --- /dev/null +++ b/examples/issue373_optional_constructor/Makefile.meson @@ -0,0 +1,6 @@ +include ../make.meson.inc + +NAME := pywrapper + +test: build + $(PYTHON) tests.py diff --git a/examples/issue373_optional_constructor/main.f90 b/examples/issue373_optional_constructor/main.f90 new file mode 100644 index 00000000..fba77cf4 --- /dev/null +++ b/examples/issue373_optional_constructor/main.f90 @@ -0,0 +1,28 @@ +module widget_mod + implicit none + public + + type :: widget_t + real :: x + real :: scale + end type widget_t + + interface widget_t + module procedure widget_init + end interface widget_t + +contains + + function widget_init(x, scale) result(w) + real, intent(in) :: x + real, intent(in), optional :: scale + type(widget_t) :: w + w%x = x + if (present(scale)) then + w%scale = scale + else + w%scale = 1.0 + end if + end function widget_init + +end module widget_mod diff --git a/examples/issue373_optional_constructor/tests.py b/examples/issue373_optional_constructor/tests.py new file mode 100644 index 00000000..b3f81720 --- /dev/null +++ b/examples/issue373_optional_constructor/tests.py @@ -0,0 +1,19 @@ +import unittest + +from pywrapper import widget_mod + + +class TestOptionalConstructor(unittest.TestCase): + def test_constructor_without_optional(self): + w = widget_mod.widget_t(3.0) + self.assertAlmostEqual(w.x, 3.0) + self.assertAlmostEqual(w.scale, 1.0) + + def test_constructor_with_optional(self): + w = widget_mod.widget_t(2.0, scale=0.5) + self.assertAlmostEqual(w.x, 2.0) + self.assertAlmostEqual(w.scale, 0.5) + + +if __name__ == "__main__": + unittest.main() diff --git a/f90wrap/directc_cgen/arguments_scalar.py b/f90wrap/directc_cgen/arguments_scalar.py index 31d3d062..0e8341df 100644 --- a/f90wrap/directc_cgen/arguments_scalar.py +++ b/f90wrap/directc_cgen/arguments_scalar.py @@ -102,7 +102,9 @@ def prepare_scalar_argument(gen: 'DirectCGenerator', arg: ft.Argument, intent: s if optional: gen.write(f"if (py_{arg.name} == Py_None) {{") gen.indent() - gen.write(f"{arg.name}_val = 0;") + # Pass a NULL pointer so the Fortran side sees present(arg) == .false. + # instead of an argument whose value happens to be zero. + gen.write(f"{arg.name} = NULL;") gen.dedent() gen.write("} else {") gen.indent() @@ -121,7 +123,11 @@ def _prepare_character_none_case( """Handle None value for character arguments.""" gen.write(f"if (py_{arg.name} == Py_None) {{") gen.indent() - if optional or intent != "in": + if optional: + # Absent optional argument: pass a NULL pointer so present(arg) == .false. + # The output path returns None for a NULL character buffer. + gen.write(f"{arg.name} = NULL;") + elif intent != "in": gen.write(f"{arg.name}_len = {default_len};") gen.write(f"if ({arg.name}_len <= 0) {{") gen.indent() diff --git a/f90wrap/directc_cgen/procedures_return.py b/f90wrap/directc_cgen/procedures_return.py index b7feb475..6f9180f0 100644 --- a/f90wrap/directc_cgen/procedures_return.py +++ b/f90wrap/directc_cgen/procedures_return.py @@ -152,7 +152,13 @@ def _prepare_character_output(gen: DirectCGenerator, arg: ft.Argument) -> None: if parsed: # Check if buffer is from numpy array gen.write(f"PyObject* py_{arg.name}_obj = NULL;") - gen.write(f"if ({arg.name}_is_array) {{") + # An absent optional argument has a NULL buffer: return None for it. + gen.write(f"if ({arg.name} == NULL) {{") + gen.indent() + gen.write("Py_INCREF(Py_None);") + gen.write(f"py_{arg.name}_obj = Py_None;") + gen.dedent() + gen.write(f"}} else if ({arg.name}_is_array) {{") gen.indent() gen.write("/* Numpy array was modified in place, no return object or free needed */") gen.dedent() diff --git a/f90wrap/transform.py b/f90wrap/transform.py index 06ad766b..757e56c3 100644 --- a/f90wrap/transform.py +++ b/f90wrap/transform.py @@ -554,12 +554,10 @@ def convert_derived_type_arguments(tree, init_lines, sizeof_fortran_t): if 'constructor' in sub.attributes: # Put the return value of the fortran constructors at first position in the arguments list if 'fortranconstructor' in sub.attributes: - j = -1 - for i, arg in enumerate(sub.arguments): - if 'optional' in arg.attributes: - j = i - break - assert(sub.arguments[j].name[:4]=='ret_') + j = next((i for i, arg in enumerate(sub.arguments) + if arg.name.startswith('ret_')), None) + assert j is not None, \ + 'no ret_ argument found in constructor %s' % sub.name sub.arguments.insert(0, sub.arguments.pop(j)) sub.arguments[0].attributes = set_intent(sub.arguments[0].attributes, 'intent(out)')