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/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ list(APPEND tests
type_bn
type_check
f2f_kind
boolean_support
)

foreach(test ${tests})
Expand Down
3 changes: 2 additions & 1 deletion examples/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ EXAMPLES = \
subroutine_contains_issue101 \
type_bn \
type_check \
f2f_kind
f2f_kind \
boolean_support

# Append callback_print_function_issue93 only if Python >= 3.11
# This test is known not to work with older python version
Expand Down
36 changes: 36 additions & 0 deletions examples/boolean_support/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#=======================================================================
# 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 --move-methods --type-check --kind-map kind.map --return-bool
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}/

main.o: ${F90_SRC}
${F90} ${F90FLAGS} -c $< -o $@

%.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
7 changes: 7 additions & 0 deletions examples/boolean_support/Makefile.meson
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
include ../make.meson.inc

NAME := pywrapper
WRAPFLAGS += -v --move-methods --type-check --kind-map kind.map

test: build
$(PYTHON) tests.py
3 changes: 3 additions & 0 deletions examples/boolean_support/kind.map
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{\
'logical':{'': 'int'},\
}
57 changes: 57 additions & 0 deletions examples/boolean_support/main.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
module m_test
implicit none
private

public :: t_bool_wrapper
public :: init
public :: free
public :: get_scalar
public :: get_static_array
public :: get_dynamic_array

type t_bool_wrapper
logical :: scalar
logical :: static_array(6)
logical, allocatable :: dynamic_array(:)
end type t_bool_wrapper

contains

subroutine init(this, scalar, static_array, dynamic_array)
type(t_bool_wrapper), intent(inout) :: this
logical, intent(in) :: scalar
logical, intent(in) :: static_array(6)
logical, intent(in) :: dynamic_array(:)

this%scalar = scalar
this%static_array = static_array
allocate(this%dynamic_array(size(dynamic_array)))
this%dynamic_array = dynamic_array
end subroutine init

subroutine free(this)
type(t_bool_wrapper), intent(inout) :: this
if (allocated(this%dynamic_array)) then
deallocate(this%dynamic_array)
end if
end subroutine free

function get_scalar(this) result(res)
type(t_bool_wrapper), intent(in) :: this
logical :: res
res = this%scalar
end function get_scalar

function get_static_array(this) result(res)
type(t_bool_wrapper), intent(in) :: this
logical :: res(6)
res = this%static_array
end function get_static_array

subroutine get_dynamic_array(this, res)
type(t_bool_wrapper), intent(in) :: this
logical, intent(out) :: res(size(this%dynamic_array))
res = this%dynamic_array
end subroutine get_dynamic_array

end module m_test
57 changes: 57 additions & 0 deletions examples/boolean_support/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import unittest
import numpy as np
from pywrapper import m_test

class TestBoolSupport(unittest.TestCase):
def setUp(self):
self.obj = m_test.t_bool_wrapper()
self.scalar_val = True
self.static_array_val = np.array([True, True, False, True, True, False], dtype=bool)
self.dynamic_array_val = np.array([False, True, True, False, True, True], dtype=bool)
self.obj.init(self.scalar_val, self.static_array_val, self.dynamic_array_val)

def tearDown(self):
self.obj.free()

def test_explicit_scalar_accessor(self):
res_scalar = self.obj.get_scalar()
self.assertEqual(res_scalar, self.scalar_val)

def test_explicit_static_array_accessor(self):
res_static = self.obj.get_static_array()
np.testing.assert_array_equal(res_static, self.static_array_val)

def test_explicit_dynamic_array_accessor(self):
res_dynamic = np.array([False, False, False, False, False, False], dtype=bool)
self.obj.get_dynamic_array(res_dynamic)
np.testing.assert_array_equal(res_dynamic, self.dynamic_array_val)

def test_implicit_scalar_accessor(self):
res_scalar = self.obj.scalar
self.assertEqual(res_scalar, self.scalar_val)

def test_implicit_static_array_accessor(self):
res_static = self.obj.static_array
np.testing.assert_array_equal(res_static, self.static_array_val)

def test_implicit_dynamic_array_accessor(self):
res_dynamic = self.obj.dynamic_array
np.testing.assert_array_equal(res_dynamic, self.dynamic_array_val)

def test_implicit_scalar_setter(self):
new_val = False
self.obj.scalar = new_val
self.assertEqual(self.obj.get_scalar(), new_val)

def test_implicit_static_array_setter(self):
new_val = np.array([False, False, True, False, False, True], dtype=bool)
self.obj.static_array = new_val
np.testing.assert_array_equal(self.obj.get_static_array(), new_val)

def test_implicit_dynamic_array_setter(self):
new_val = np.array([True, False, False, True, False, False], dtype=bool)
self.obj.dynamic_array = new_val
np.testing.assert_array_equal(self.obj.dynamic_array, new_val)

if __name__ == '__main__':
unittest.main()
84 changes: 64 additions & 20 deletions f90wrap/pywrapgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -676,8 +676,38 @@ def visit_Procedure(self, node):
self.indent()
self.write(self._format_doc_string(node))

# Convert python booleans to 4 bytes integer to match fortran logical memory layout
# python_bool_to_int maps the original argument name to the new variable name used for the integer version of the argument
# Needed to convert back to bool and for type checking
python_bool_to_int = dict()
for arg in self._filtered_arguments:
if arg.type == 'logical':
numpy_type = ft.f2numpy_type(arg.type, self.kind_map)
test_is_numpy_array = f"isinstance({arg.py_name},(numpy.ndarray, numpy.generic))"
test_is_bool = f"{arg.py_name}.dtype.num == numpy.dtype(bool).num"
if "intent(in)" in arg.attributes:
self.write(f"if {test_is_numpy_array} and {test_is_bool}:")
self.indent()
self.write("{0} = {0}.astype('{1}')".format(arg.py_name, numpy_type))
self.dedent()
elif "intent(inout)" in arg.attributes:
python_bool_to_int[arg.py_name] = f"{arg.py_name}_to_int"
self.write(f"if {test_is_numpy_array} and {test_is_bool}:")
self.indent()
self.write("{0} = {1}.astype('{2}')".format(python_bool_to_int[arg.py_name], arg.py_name, numpy_type))
self.dedent()
self.write("else:")
self.indent()
self.write("{0} = {1}".format(python_bool_to_int[arg.py_name], arg.py_name))
self.dedent()

dct['f90_arg_names'] = dct['f90_arg_names'].replace(
f"={arg.py_name}",
f"={python_bool_to_int[arg.py_name]}"
)

if self.type_check:
self.write_type_checks(node)
self.write_type_checks(node, python_bool_to_int)

for arg in self._filtered_arguments:
if "optional" in arg.attributes and "._handle" in arg.py_value:
Expand Down Expand Up @@ -756,6 +786,10 @@ def f902py_name(node, f90_name):
)
self.write(call_line)

for arg in self._filtered_arguments:
if arg.type == 'logical' and 'intent(inout)' in arg.attributes:
self.write(f"{arg.py_name}[...] = {python_bool_to_int[arg.py_name]}.astype(bool)")

if isinstance(node, ft.Function):
# convert any derived type return values to Python objects
for ret_val in self._filtered_ret_val:
Expand Down Expand Up @@ -783,15 +817,23 @@ def f902py_name(node, f90_name):
self.write("%s._setup_finalizer()" % ret_val.name)
# strip white space for string returns
pytype = ft.f2py_type(ret_val.type)
dims = list(filter(lambda x: x.startswith("dimension"), ret_val.attributes))
if self.return_decoded and pytype == "str":
dct["result"] = dct["result"].replace(
ret_val.name, '%s.strip().decode("utf-8")' % ret_val.name
)
# convert back Fortran logical to Python bool
if self.return_bool and ret_val.type == "logical":
dct["result"] = dct["result"].replace(
ret_val.name, 'bool(%s)' % ret_val.name
)
if len(dims) > 0:
# array of logicals
dct["result"] = dct["result"].replace(
ret_val.name, '%s.astype(bool)' % ret_val.name
)
else:
# single logical
dct["result"] = dct["result"].replace(
ret_val.name, 'bool(%s)' % ret_val.name
)

if dct["result"]:
self.write("return %(result)s" % dct)
Expand Down Expand Up @@ -1407,14 +1449,15 @@ def write_dt_array_wrapper(self, node, el, dims):
self.dedent()
self.write()

def write_type_checks(self, node):
def write_type_checks(self, node, python_bool_to_int):
# This adds tests that checks data types and dimensions
# to ensure either the correct version of an interface is used
# either an exception is returned
for arg in self._filtered_arguments:
arg_py_name = python_bool_to_int.get(arg.py_name, arg.py_name)
# Check if optional argument is being passed
if "optional" in arg.attributes:
self.write("if {0} is not None:".format(arg.py_name))
self.write("if {0} is not None:".format(arg_py_name))
self.indent()

ft_array_dim_list = list(
Expand All @@ -1440,11 +1483,11 @@ def write_type_checks(self, node):
)
self.write(
"if not isinstance({0}, {1}.{2}) :".format(
arg.py_name, cls_mod_name, cls_name
arg_py_name, cls_mod_name, cls_name
)
)
self.indent()
self.write(f"msg = f\"Expecting '{{{cls_mod_name}.{cls_name}}}' but got '{{type({arg.py_name})}}'\"")
self.write(f"msg = f\"Expecting '{{{cls_mod_name}.{cls_name}}}' but got '{{type({arg_py_name})}}'\"")
self.write(f"raise TypeError(msg)")
self.dedent()

Expand All @@ -1453,16 +1496,17 @@ def write_type_checks(self, node):
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
# Good enough for now if it works on standard 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
arg_py_name
)
)
self.indent()
Expand All @@ -1484,14 +1528,14 @@ def write_type_checks(self, node):
if ft_array_dim == 0 and "intent(in)" in arg.attributes:
self.write(
"if not interface_call and {0}.dtype.num in {{{1}}}:".format(
arg.py_name,
arg_py_name,
", ".join(
[str(atype().dtype.num) for atype in convertible_types]
),
)
)
self.indent()
self.write("{0} = {0}.astype('{1}')".format(arg.py_name, pytype))
self.write("{0} = {0}.astype('{1}')".format(arg_py_name, pytype))
self.dedent()

# Allow fortran character to match python ubyte, unicode_ or string_
Expand All @@ -1514,23 +1558,23 @@ def write_type_checks(self, node):
if ft_array_dim == -1:
self.write(
"if {0}.dtype.num not in {{{1}}}:".format(
arg.py_name, ",".join(str_types)
arg_py_name, ",".join(str_types)
)
)
else:
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)
arg_py_name, ",".join(str_dims), ",".join(str_types)
)
)
elif ft_array_dim == -1:
self.write(
"if {0}.dtype.num != {1}:".format(arg.py_name, array_type)
"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
arg_py_name, str(ft_array_dim), array_type
)
)

Expand All @@ -1542,21 +1586,21 @@ def write_type_checks(self, node):
ft.f2py_type(arg.type),
array_type,
str(ft_array_dim),
arg.py_name,
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)
arg_py_name, ft.f2py_type(arg.type)
)
)
self.indent()
self.write(
"raise TypeError(\"Expecting '{0}' but got '%s'\"%type({1}))".format(
ft.f2py_type(arg.type), arg.py_name
ft.f2py_type(arg.type), arg_py_name
)
)
self.dedent()
Expand All @@ -1565,7 +1609,7 @@ def write_type_checks(self, node):
self.indent()
self.write(
"raise TypeError(\"Expecting numpy array but got '%s'\"%type({0}))".format(
arg.py_name
arg_py_name
)
)
self.dedent()
Expand Down
Loading