Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ EXAMPLES = \
issue32 \
issue353_selected_kind \
issue357_name_conflict \
issue371_optional_complex \
issue41_abstract_classes \
keep_single_interface \
keyword_renaming_issue160 \
Expand Down
33 changes: 33 additions & 0 deletions examples/issue371_optional_complex/Makefile
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions examples/issue371_optional_complex/Makefile.meson
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
include ../make.meson.inc

NAME := pywrapper

test: build
$(PYTHON) tests.py
21 changes: 21 additions & 0 deletions examples/issue371_optional_complex/main.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
! Regression example for optional complex scalar arguments.
!
! f90wrap previously dropped every optional complex scalar argument in
! transform.py, so present(z) could never be exercised from Python. Required
! complex scalars were always supported. This passes a complex value through
! present(z) on both the regular f2py path and the --direct-c path.
module m_complex
implicit none
contains

subroutine opt_complex(flag, z)
integer, intent(out) :: flag
complex, intent(in), optional :: z
if (present(z)) then
flag = nint(real(z)) + nint(aimag(z))
else
flag = -1
end if
end subroutine opt_complex

end module m_complex
17 changes: 17 additions & 0 deletions examples/issue371_optional_complex/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import unittest

from pywrapper import m_complex


class TestOptionalComplex(unittest.TestCase):
def test_present_single(self):
# Previously impossible: the optional complex argument was dropped
# entirely in transform.py before it reached either backend.
self.assertEqual(m_complex.opt_complex(z=complex(2, 3)), 5)

def test_present_negativeimag(self):
self.assertEqual(m_complex.opt_complex(z=complex(4, -1)), 3)


if __name__ == "__main__":
unittest.main()
5 changes: 5 additions & 0 deletions f90wrap/directc_cgen/arguments_scalar.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ def _write_scalar_number_handling(gen: 'DirectCGenerator', arg: ft.Argument, c_t
gen.write(f"{arg.name}_val = ({c_type})PyFloat_AsDouble(py_{arg.name});")
elif fmt == "p":
gen.write(f"{arg.name}_val = ({c_type})PyObject_IsTrue(py_{arg.name});")
elif fmt == "D":
gen.write(f"Py_complex {arg.name}_pc = PyComplex_AsCComplex(py_{arg.name});")
gen.write(
f"{arg.name}_val = ({c_type})({arg.name}_pc.real + {arg.name}_pc.imag * _Complex_I);"
)
else:
gen.write(
f'PyErr_SetString(PyExc_TypeError, "Unsupported argument {arg.name}");'
Expand Down
16 changes: 14 additions & 2 deletions f90wrap/numpy_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ def _normalize_fortran_type(ftype: str) -> Tuple[str, Optional[str], Dict[str, b
if not kind_str:
kind_str = "8"

if base == "double complex":
base = "complex"
modifiers["force_double"] = True
if not kind_str:
kind_str = "8"

return base, kind_str, modifiers


Expand Down Expand Up @@ -103,7 +109,10 @@ def numpy_type_from_fortran(ftype: str, kind_map: Dict[str, Dict[str, str]]) ->
return "NPY_CLONGDOUBLE"
if bits >= 8:
return "NPY_CDOUBLE"
return "NPY_CDOUBLE"
return "NPY_COMPLEX64"
if force_double:
return "NPY_CDOUBLE"
return "NPY_COMPLEX64"

elif base == "character":
return "NPY_STRING"
Expand Down Expand Up @@ -172,7 +181,10 @@ def c_type_from_fortran(ftype: str, kind_map: Dict[str, Dict[str, str]]) -> str:
return "long double _Complex"
if bits >= 8:
return "double _Complex"
return "double _Complex"
return "float _Complex"
if force_double:
return "double _Complex"
return "float _Complex"

elif base == "character":
return "char"
Expand Down
5 changes: 0 additions & 5 deletions f90wrap/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,11 +332,6 @@ def visit_Argument(self, node):

dims = [attrib for attrib in node.attributes if attrib.startswith('dimension')]

# remove optional complex scalar arguments
if node.type.startswith('complex') and len(dims) == 0:
log.warning('removing optional argument %s as it is a complex scalar' % node.name)
return None

# remove optional derived types not in self.types
typename = ft.derived_typename(node.type)
if typename and typename not in self.types:
Expand Down
13 changes: 13 additions & 0 deletions test/samples/optional_complex.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module optional_complex
implicit none
contains
subroutine opt_complex(flag, z)
integer, intent(out) :: flag
complex, intent(in), optional :: z
if (present(z)) then
flag = nint(real(z)) + nint(aimag(z))
else
flag = -1
end if
end subroutine opt_complex
end module optional_complex
20 changes: 16 additions & 4 deletions test/test_directc.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,14 @@ def test_numpy_type_from_fortran_logical(self):
self.assertEqual(numpy_utils.numpy_type_from_fortran('logical', {}), 'NPY_INT32')

def test_numpy_type_from_fortran_complex(self):
"""Test complex type mapping to NumPy."""
self.assertEqual(numpy_utils.numpy_type_from_fortran('complex', {}), 'NPY_CDOUBLE')
"""Test complex type mapping to NumPy.

Default complex is two default reals (single precision), so it maps to
NPY_COMPLEX64, mirroring real -> NPY_FLOAT32.
"""
self.assertEqual(numpy_utils.numpy_type_from_fortran('complex', {}), 'NPY_COMPLEX64')
self.assertEqual(numpy_utils.numpy_type_from_fortran('complex(4)', {}), 'NPY_COMPLEX64')
self.assertEqual(numpy_utils.numpy_type_from_fortran('double complex', {}), 'NPY_CDOUBLE')
self.assertEqual(
numpy_utils.numpy_type_from_fortran('complex(dp)', self.kind_map),
'NPY_COMPLEX128'
Expand Down Expand Up @@ -84,8 +90,14 @@ def test_c_type_from_fortran_logical(self):
self.assertEqual(numpy_utils.c_type_from_fortran('logical', {}), 'int')

def test_c_type_from_fortran_complex(self):
"""Test complex type mapping to C."""
self.assertEqual(numpy_utils.c_type_from_fortran('complex', {}), 'double _Complex')
"""Test complex type mapping to C.

Default complex is single precision (two default reals), so it maps to
float _Complex, mirroring real -> float.
"""
self.assertEqual(numpy_utils.c_type_from_fortran('complex', {}), 'float _Complex')
self.assertEqual(numpy_utils.c_type_from_fortran('complex(4)', {}), 'float _Complex')
self.assertEqual(numpy_utils.c_type_from_fortran('double complex', {}), 'double _Complex')
self.assertEqual(
numpy_utils.c_type_from_fortran('complex(sp)', self.kind_map),
'float _Complex'
Expand Down
11 changes: 11 additions & 0 deletions test/test_transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,17 @@ def test_shorten_long_name(self):
# Same input should produce same output (deterministic)
self.assertEqual(shorten_long_name(long_name), shortened)

def test_optional_complex_scalar_kept(self):
'''
Optional complex scalar arguments must not be dropped.
Regression test for issue #371.
'''
root = parser.read_files([str(test_samples_dir/'optional_complex.f90')])
root = transform.UnwrappablesRemover([], [], [], [], []).visit(root)

sub = next(p for p in root.modules[0].procedures if p.name == 'opt_complex')
self.assertIn('z', [arg.name for arg in sub.arguments])

def test_kind_parameter_uses_clause(self):
'''
Verify that kind parameters used in procedure arguments are imported.
Expand Down
Loading