diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index c27c3154..58681a75 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -26,6 +26,9 @@ list(APPEND tests subroutine_contains_issue101 type_bn kind_map_default + docstring + return_array + intent_out_size ) foreach(test ${tests}) @@ -36,5 +39,3 @@ foreach(test ${tests}) WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}/${test}" ) endforeach() - - diff --git a/examples/Makefile b/examples/Makefile index a3cac481..28a8b9b2 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -23,9 +23,11 @@ EXAMPLES = arrayderivedtypes \ docstring \ type_check \ derivedtypes_procedure \ + return_array \ optional_string \ long_subroutine_name \ - kind_map_default + kind_map_default \ + intent_out_size PYTHON = python 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/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/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/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 ca0c9842..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): """ @@ -912,9 +942,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 +966,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): """ 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..d25ec86d 100644 --- a/f90wrap/pywrapgen.py +++ b/f90wrap/pywrapgen.py @@ -24,8 +24,8 @@ import os import logging import re +import numpy as np -from f90wrap.transform import ArrayDimensionConverter from f90wrap.transform import shorten_long_name from f90wrap import fortran as ft from f90wrap import codegen as cg @@ -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) @@ -513,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"], + "f90wrap_n%d=%s" % (offset, out_dim), + ) + offset += 1 + call_line = ( "%(call)s%(mod_name)s.%(subroutine_name)s(%(f90_arg_names)s)" % dct ) @@ -574,7 +565,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 +614,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 +685,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 +770,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 +810,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 +828,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"]) @@ -880,10 +871,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 @@ -902,7 +890,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 +1020,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 + ['"""']) diff --git a/f90wrap/transform.py b/f90wrap/transform.py index ef832120..06c4fad9 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,29 +577,10 @@ 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 + all_new_dummy_args = [] for arg in node.arguments: dims = [attr for attr in arg.attributes if attr.startswith('dimension')] if dims == []: @@ -607,7 +588,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 = [] @@ -621,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)' % @@ -634,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): @@ -885,13 +871,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, @@ -924,13 +911,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: @@ -990,7 +991,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 @@ -1415,7 +1417,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 +1466,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