From e2145a0aec0c8263b0d5151b5bed9236978ec7d6 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 11 Sep 2025 15:37:38 +0200 Subject: [PATCH 01/11] Get fortran compiling --- meson.build | 3 +- src/example_fgen_basic/error_v/error_v.f90 | 11 +- .../pyfgen_runtime/exceptions.py | 6 + src/example_fgen_basic/result/result.f90 | 44 ++---- .../result/result0D_int.f90 | 98 ------------- src/example_fgen_basic/result/result_int.f90 | 93 +++++++++---- .../result/result_int1D.f90 | 129 ++++++++++++++++++ src/example_fgen_basic/result/result_none.f90 | 89 ++++++++++++ tests/unit/test_error_v_creation.py | 13 +- 9 files changed, 316 insertions(+), 170 deletions(-) delete mode 100644 src/example_fgen_basic/result/result0D_int.f90 create mode 100644 src/example_fgen_basic/result/result_int1D.f90 create mode 100644 src/example_fgen_basic/result/result_none.f90 diff --git a/meson.build b/meson.build index 8ecff13..07400b1 100644 --- a/meson.build +++ b/meson.build @@ -69,8 +69,9 @@ if pyprojectwheelbuild_enabled 'src/example_fgen_basic/get_wavelength.f90', 'src/example_fgen_basic/kind_parameters.f90', 'src/example_fgen_basic/result/result.f90', - 'src/example_fgen_basic/result/result0D_int.f90', 'src/example_fgen_basic/result/result_int.f90', + 'src/example_fgen_basic/result/result_int1D.f90', + 'src/example_fgen_basic/result/result_none.f90', ) # All Python files (wrappers and otherwise) diff --git a/src/example_fgen_basic/error_v/error_v.f90 b/src/example_fgen_basic/error_v/error_v.f90 index f9eb732..1904e4c 100644 --- a/src/example_fgen_basic/error_v/error_v.f90 +++ b/src/example_fgen_basic/error_v/error_v.f90 @@ -37,7 +37,7 @@ module m_error_v procedure, public :: build procedure, public :: finalise - final :: clean_up + final :: finalise_auto ! get_res sort of not needed (?) ! get_err sort of not needed (?) @@ -95,14 +95,17 @@ subroutine finalise(self) end subroutine finalise - subroutine clean_up(self) + subroutine finalise_auto(self) !! Finalise the instance (i.e. free/deallocate) + !! + !! This method is expected to be called automatically + !! by clever clean up, which is why it differs from [TODO x-ref] `finalise` type(ErrorV), intent(inout) :: self ! Hopefully can leave without docstring (like Python) - call self%finalise() + call self % finalise() - end subroutine clean_up + end subroutine finalise_auto end module m_error_v diff --git a/src/example_fgen_basic/pyfgen_runtime/exceptions.py b/src/example_fgen_basic/pyfgen_runtime/exceptions.py index 0edd2ed..9a34751 100644 --- a/src/example_fgen_basic/pyfgen_runtime/exceptions.py +++ b/src/example_fgen_basic/pyfgen_runtime/exceptions.py @@ -18,6 +18,12 @@ def __init__(self, compiled_extension_name: str): super().__init__(error_msg) +class FortranError(Exception): + """ + Base class for errors that originated on the Fortran side + """ + + class MissingOptionalDependencyError(ImportError): """ Raised when an optional dependency is missing diff --git a/src/example_fgen_basic/result/result.f90 b/src/example_fgen_basic/result/result.f90 index 03e7df2..59991ba 100644 --- a/src/example_fgen_basic/result/result.f90 +++ b/src/example_fgen_basic/result/result.f90 @@ -9,23 +9,15 @@ module m_result implicit none (type, external) private - type, abstract, public :: Result + type, abstract, public :: ResultBase !! Result type !! !! Holds either the result or an error. ! class(*), allocatable :: data_v(..) - ! MZ: assumed rank can only be dummy argument NOT type/class argument - ! Data i.e. the result (if no error occurs) - ! - ! Assumed rank array - ! (https://fortran-lang.discourse.group/t/assumed-rank-arrays/1049) - ! Technically a Fortran 2018 feature, - ! so maybe we need to update our file extensions. - ! If we can't use this, just comment this out - ! and leave each subclass of Result to set its data type - ! (e.g. ResultInteger will have `integer :: data`, - ! ResultDP1D will have `real(dp), dimension(:), allocatable :: data`) + ! assumed rank can only be dummy argument NOT type/class argument + ! hence leave this undefined + ! Sub-classes have to define what kind of data value they support class(ErrorV), allocatable :: error_v !! Error @@ -34,35 +26,27 @@ module m_result private + ! Expect sub-classes to implement ! procedure, public:: build - ! TODO: Think about whether build should be on the abstract class - ! or just on each concrete implementation procedure, public :: is_error - procedure, public :: clean_up + ! Expect sub-classes to implement + ! procedure, public :: finalise + ! final :: finalise_auto - end type Result + end type ResultBase - ! interface Result - !! Constructor interface - see build (TODO: figure out cross-ref syntax) for details + ! Expect sub-classes to implement + ! interface ResultSubClass + !! Constructor interface - see build [cross-ref goes here] for details ! module procedure :: constructor - ! end interface Result + ! end interface ResultSubClass contains - subroutine clean_up(self) - !! Finalise the instance (i.e. free/deallocate) - - class(Result), intent(inout) :: self - ! Hopefully can leave without docstring (like Python) - - deallocate (self % error_v) - - end subroutine clean_up - pure function is_error(self) result(is_err) !! Determine whether `self` contains an error or not - class(Result), intent(in) :: self + class(ResultBase), intent(in) :: self ! Hopefully can leave without docstring (like Python) logical :: is_err diff --git a/src/example_fgen_basic/result/result0D_int.f90 b/src/example_fgen_basic/result/result0D_int.f90 deleted file mode 100644 index df7d899..0000000 --- a/src/example_fgen_basic/result/result0D_int.f90 +++ /dev/null @@ -1,98 +0,0 @@ -!> Result value for integers -!> -!> Inspired by the excellent, MIT licensed -!> https://github.com/samharrison7/fortran-error-handler -module m_result_int - - use m_error_v, only: ErrorV - use m_result, only: Result_base - - implicit none (type, external) - private - - type, extends(Result_base), public :: ResultInteger0D - !! Result type that holds integer values - !! - !! Holds either an integer value or an error. - - integer, allocatable :: data_v - !! Data i.e. the result (if no error occurs) - - ! class(ErrorV), allocatable :: error_v - !! Error - - contains - - private - - procedure, public :: build - ! `finalise` and `is_error` come from abstract base class - final :: finalise - - end type ResultInteger0D - - interface ResultInteger0D - !! Constructor interface - see build (TODO: figure out cross-ref syntax) for details - module procedure :: constructor - end interface ResultInteger0D - -contains - - function constructor(data_v, error_v) result(self) - !! Build instance - - type(ResultInteger0D) :: self - ! Hopefully can leave without docstring (like Python) - - class(ErrorV), intent(inout), optional :: error_v - !! Error message - - integer, intent(in), optional :: data_v - !! Data - - call self % build(data_v_in=data_v, error_v_in=error_v) - - end function constructor - - function build(data_v_in, error_v_in) result(res) - !! Build instance - - class(Result_base), intent(out) :: res - ! Hopefully can leave without docstring (like Python) - - integer, intent(in), optional :: data_v_in - !! Data - - class(ErrorV), intent(inout), optional :: error_v_in - !! Error message - - if (present(data_v_in) .and. present(error_v_in)) then - allocate(Result_base :: res) - res % error_v % message = "Both data and error were provided" - else if (present(data_v_in)) then - allocate (ResultInteger0D :: res) - allocate (self % data_v, source=data_v_in) - ! No error - no need to call res % build - else if (present(error_v_in)) then - allocate(Result_base :: res) - allocate (res % error_v, source=error_v_in) - ! No error - no need to call res % build - else - allocate(Result_base :: res) - res % error_v % message = "Neither data nor error were provided" - end if - - end function build - - subroutine finalise(self) - !! Finalise instance - - type(ResultInteger0D), intent(inout) :: self - ! Hopefully can leave without docstring (like Python) - - if (allocated(self % data_v)) deallocate (self % data_v) - call self % clean_up() - - end subroutine finalise - -end module m_result_int diff --git a/src/example_fgen_basic/result/result_int.f90 b/src/example_fgen_basic/result/result_int.f90 index 960db66..998b715 100644 --- a/src/example_fgen_basic/result/result_int.f90 +++ b/src/example_fgen_basic/result/result_int.f90 @@ -1,94 +1,127 @@ -!> Result value for integers +!> Result type for integers !> !> Inspired by the excellent, MIT licensed !> https://github.com/samharrison7/fortran-error-handler module m_result_int use m_error_v, only: ErrorV - use m_result, only: Result + use m_result, only: ResultBase + use m_result_none, only: ResultNone implicit none (type, external) private - type, extends(Result), public :: ResultInteger1D + type, extends(ResultBase), public :: ResultInt !! Result type that holds integer values - !! - !! Holds either an integer value or an error. - integer, allocatable :: data_v(:) + integer, allocatable :: data_v !! Data i.e. the result (if no error occurs) - ! class(ErrorV), allocatable :: error_v - !! Error + ! Note: the error_v attribute comes from ResultBase contains private procedure, public :: build - ! `finalise` and `is_error` come from abstract base class - final :: finalise + procedure, public :: finalise + final :: finalise_auto - end type ResultInteger1D + end type ResultInt - interface ResultInteger1D - !! Constructor interface - see build (TODO: figure out cross-ref syntax) for details + interface ResultInt + !! Constructor interface - see build [TODO: x-ref] for details module procedure :: constructor - end interface ResultInteger1D + end interface ResultInt contains function constructor(data_v, error_v) result(self) !! Build instance - type(ResultInteger1D) :: self + type(ResultInt) :: self ! Hopefully can leave without docstring (like Python) - class(ErrorV), intent(inout), optional :: error_v - !! Error message - - integer, allocatable, intent(in), optional :: data_v(:) + integer, intent(in), optional :: data_v !! Data - call self % build(data_v_in=data_v, error_v_in=error_v) + class(ErrorV), intent(in), optional :: error_v + !! Error + + type(ResultNone) :: build_res + + build_res = self % build(data_v_in=data_v, error_v_in=error_v) + + if (build_res % is_error()) then + + ! This interface has to return the initialised object, + ! it cannot return a Result type, + ! so we have no choice but to raise a fatal error here. + print *, build_res % error_v % message + error stop build_res % error_v % code + + ! else + ! Assume no error occurred and initialisation was fine + + end if end function constructor - subroutine build(self, data_v_in, error_v_in) + function build(self, data_v_in, error_v_in) result(res) !! Build instance - class(ResultInteger1D), intent(inout) :: self + class(ResultInt), intent(out) :: self ! Hopefully can leave without docstring (like Python) - integer, intent(in), optional :: data_v_in(:) + integer, intent(in), optional :: data_v_in !! Data - class(ErrorV), intent(inout), optional :: error_v_in + class(ErrorV), intent(in), optional :: error_v_in !! Error message + type(ResultNone) :: res + !! Result + if (present(data_v_in) .and. present(error_v_in)) then - error_v_in % message = "Both data and error were provided" + res % error_v % message = "Both data and error were provided" + else if (present(data_v_in)) then allocate (self % data_v, source=data_v_in) ! No error - no need to call res % build + else if (present(error_v_in)) then allocate (self % error_v, source=error_v_in) ! No error - no need to call res % build + else - error_v_in % message = "Neither data nor error were provided" + res % error_v % message = "Neither data nor error were provided" + end if - end subroutine build + end function build subroutine finalise(self) - !! Finalise instance + !! Finalise the instance (i.e. free/deallocate) - type(ResultInteger1D), intent(inout) :: self + class(ResultInt), intent(inout) :: self ! Hopefully can leave without docstring (like Python) if (allocated(self % data_v)) deallocate (self % data_v) - if (allocated(self % error_v)) call self % clean_up() + if (allocated(self % error_v)) deallocate(self % error_v) end subroutine finalise + subroutine finalise_auto(self) + !! Finalise the instance (i.e. free/deallocate) + !! + !! This method is expected to be called automatically + !! by clever clean up, which is why it differs from [TODO x-ref] `finalise` + + type(ResultInt), intent(inout) :: self + ! Hopefully can leave without docstring (like Python) + + call self % finalise() + + end subroutine finalise_auto + end module m_result_int diff --git a/src/example_fgen_basic/result/result_int1D.f90 b/src/example_fgen_basic/result/result_int1D.f90 new file mode 100644 index 0000000..86a6192 --- /dev/null +++ b/src/example_fgen_basic/result/result_int1D.f90 @@ -0,0 +1,129 @@ +!> Result value for 1D arrays of integers +!> +!> Inspired by the excellent, MIT licensed +!> https://github.com/samharrison7/fortran-error-handler +module m_result_int1d + + use m_error_v, only: ErrorV + use m_result, only: ResultBase + use m_result_none, only: ResultNone + + implicit none (type, external) + private + + type, extends(ResultBase), public :: ResultInt1D + !! Result type that holds integer values + !! + !! Holds either an integer value or an error. + + integer, allocatable, dimension(:) :: data_v + !! Data i.e. the result (if no error occurs) + + ! Note: the error_v attribute comes from ResultBase + + contains + + private + + procedure, public :: build + procedure, public :: finalise + final :: finalise_auto + + end type ResultInt1D + + interface ResultInt1D + !! Constructor interface - see build [TODO: x-ref] for details + module procedure :: constructor + end interface ResultInt1D + +contains + + function constructor(data_v, error_v) result(self) + !! Build instance + + type(ResultInt1D) :: self + ! Hopefully can leave without docstring (like Python) + + integer, allocatable, intent(in), dimension(:), optional :: data_v + !! Data + + class(ErrorV), intent(in), optional :: error_v + !! Error message + + type(ResultNone) :: build_res + + build_res = self % build(data_v_in=data_v, error_v_in=error_v) + + if (build_res % is_error()) then + + ! This interface has to return the initialised object, + ! it cannot return a Result type, + ! so we have no choice but to raise a fatal error here. + print *, build_res % error_v % message + error stop build_res % error_v % code + + ! else + ! Assume no error occurred and initialisation was fine + + end if + + end function constructor + + function build(self, data_v_in, error_v_in) result(res) + !! Build instance + + class(ResultInt1D), intent(inout) :: self + ! Hopefully can leave without docstring (like Python) + + integer, intent(in), dimension(:), optional :: data_v_in + !! Data + + class(ErrorV), intent(in), optional :: error_v_in + !! Error message + + type(ResultNone) :: res + !! Result + + if (present(data_v_in) .and. present(error_v_in)) then + res % error_v % message = "Both data and error were provided" + + else if (present(data_v_in)) then + allocate (self % data_v, source=data_v_in) + ! No error - no need to call res % build + + else if (present(error_v_in)) then + allocate (self % error_v, source=error_v_in) + ! No error - no need to call res % build + + else + res % error_v % message = "Neither data nor error were provided" + + end if + + end function build + + subroutine finalise(self) + !! Finalise the instance (i.e. free/deallocate) + + class(ResultInt1D), intent(inout) :: self + ! Hopefully can leave without docstring (like Python) + + if (allocated(self % data_v)) deallocate (self % data_v) + if (allocated(self % error_v)) deallocate(self % error_v) + + end subroutine finalise + + subroutine finalise_auto(self) + !! Finalise the instance (i.e. free/deallocate) + !! + !! This method is expected to be called automatically + !! by clever clean up, which is why it differs from [TODO x-ref] `finalise` + + type(ResultInt1D), intent(inout) :: self + ! Hopefully can leave without docstring (like Python) + + call self % finalise() + + end subroutine finalise_auto + +end module m_result_int1d diff --git a/src/example_fgen_basic/result/result_none.f90 b/src/example_fgen_basic/result/result_none.f90 new file mode 100644 index 0000000..941ac08 --- /dev/null +++ b/src/example_fgen_basic/result/result_none.f90 @@ -0,0 +1,89 @@ +!> Result value where no data is carried around +!> +!> Inspired by the excellent, MIT licensed +!> https://github.com/samharrison7/fortran-error-handler +module m_result_none + + use m_error_v, only: ErrorV + use m_result, only: ResultBase + + implicit none (type, external) + private + + type, extends(ResultBase), public :: ResultNone + !! Result type that cannot hold data + + contains + + private + + procedure, public :: build + procedure, public :: finalise + final :: finalise_auto + + end type ResultNone + + interface ResultNone + module procedure :: constructor + end interface ResultNone + +contains + + function constructor(error_v) result(self) + !! Build instance + + type(ResultNone) :: self + ! Hopefully can leave without docstring (like Python) + + class(ErrorV), intent(in), optional :: error_v + !! Error message + + call self % build(error_v_in=error_v) + + end function constructor + + subroutine build(self, error_v_in) + !! Build instance + + class(ResultNone), intent(inout) :: self + ! Hopefully can leave without docstring (like Python) + + class(ErrorV), intent(in), optional :: error_v_in + !! Error message + + if (present(error_v_in)) then + allocate (self % error_v, source=error_v_in) + ! No error - no need to call res % build + + ! else + ! ! Special case - users can initialise ResultNone without an error if they want + ! res % error_v % message = "No error was provided" + + end if + + end subroutine build + + subroutine finalise(self) + !! Finalise the instance (i.e. free/deallocate) + + class(ResultNone), intent(inout) :: self + ! Hopefully can leave without docstring (like Python) + + if (allocated(self % error_v)) deallocate(self % error_v) + + end subroutine finalise + + subroutine finalise_auto(self) + !! Finalise the instance (i.e. free/deallocate) + !! + !! This method is expected to be called automatically + !! by clever clean up, which is why it differs from [TODO x-ref] `finalise` + + type(ResultNone), intent(inout) :: self + ! Hopefully can leave without docstring (like Python) + + call self % finalise() + + end subroutine finalise_auto + +end module m_result_none diff --git a/tests/unit/test_error_v_creation.py b/tests/unit/test_error_v_creation.py index b3d3c7e..e272d13 100644 --- a/tests/unit/test_error_v_creation.py +++ b/tests/unit/test_error_v_creation.py @@ -3,9 +3,11 @@ """ import numpy as np +import pytest from example_fgen_basic.error_v import ErrorV from example_fgen_basic.error_v.creation import create_error, create_errors +from example_fgen_basic.pyfgen_runtime.exceptions import FortranError def test_create_error_odd(): @@ -27,13 +29,10 @@ def test_create_error_even(): assert res.message == "Even number supplied" -def test_create_error_negative(): - res = create_error(-1.0) - - assert isinstance(res, ErrorV) - - assert res.code == 2 - assert res.message == "Negative number supplied" +def test_create_error_negative_raises(): + # TODO: switch to more precise error type + with pytest.raises(FortranError): + create_error(-1.0) def test_create_error_lots_of_repeated_calls(): From 7937334a415f2d832c3db13bd01b6311cc202bb7 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 11 Sep 2025 15:47:49 +0200 Subject: [PATCH 02/11] Add square root for easier illustration --- meson.build | 2 + scripts/inject-srcs-into-meson-build.py | 1 + src/example_fgen_basic/get_square_root.f90 | 36 ++++++ src/example_fgen_basic/result/result_dp.f90 | 128 ++++++++++++++++++++ tests/unit/test_error_v_creation.py | 1 + 5 files changed, 168 insertions(+) create mode 100644 src/example_fgen_basic/get_square_root.f90 create mode 100644 src/example_fgen_basic/result/result_dp.f90 diff --git a/meson.build b/meson.build index 07400b1..bc1b5c3 100644 --- a/meson.build +++ b/meson.build @@ -66,9 +66,11 @@ if pyprojectwheelbuild_enabled 'src/example_fgen_basic/error_v/passing.f90', 'src/example_fgen_basic/fpyfgen/base_finalisable.f90', 'src/example_fgen_basic/fpyfgen/derived_type_manager_helpers.f90', + 'src/example_fgen_basic/get_square_root.f90', 'src/example_fgen_basic/get_wavelength.f90', 'src/example_fgen_basic/kind_parameters.f90', 'src/example_fgen_basic/result/result.f90', + 'src/example_fgen_basic/result/result_dp.f90', 'src/example_fgen_basic/result/result_int.f90', 'src/example_fgen_basic/result/result_int1D.f90', 'src/example_fgen_basic/result/result_none.f90', diff --git a/scripts/inject-srcs-into-meson-build.py b/scripts/inject-srcs-into-meson-build.py index 3487b16..5041b53 100644 --- a/scripts/inject-srcs-into-meson-build.py +++ b/scripts/inject-srcs-into-meson-build.py @@ -93,6 +93,7 @@ def main(): meson_variable, sorted(src_paths), REPO_ROOT ) + # TODO: something wrong in here meson_build_out = re.sub(pattern, substitution, meson_build_out) with open(REPO_ROOT / "meson.build", "w") as fh: diff --git a/src/example_fgen_basic/get_square_root.f90 b/src/example_fgen_basic/get_square_root.f90 new file mode 100644 index 0000000..3093659 --- /dev/null +++ b/src/example_fgen_basic/get_square_root.f90 @@ -0,0 +1,36 @@ +!> Get square root of a number +module m_get_square_root + + use kind_parameters, only: dp + use m_error_v, only: ErrorV + use m_result_dp, only: ResultDP + + implicit none(type, external) + private + + public :: get_square_root + +contains + + function get_square_root(inv) result(res) + !! Get square root of a number + + real(kind=dp), intent(in) :: inv + !! Frequency + + type(ResultDP) :: res + !! Result + !! + !! Square root if the number is positive or zero. + !! Error otherwise. + + if (inv >= 0) then + res = ResultDP(data_v=sqrt(inv)) + else + ! TODO: include input value in the message + res = ResultDP(error_v=ErrorV(code=1, message="Input value was negative")) + end if + + end function get_square_root + +end module m_get_square_root diff --git a/src/example_fgen_basic/result/result_dp.f90 b/src/example_fgen_basic/result/result_dp.f90 new file mode 100644 index 0000000..da31601 --- /dev/null +++ b/src/example_fgen_basic/result/result_dp.f90 @@ -0,0 +1,128 @@ +!> Result type for double precision real values +!> +!> Inspired by the excellent, MIT licensed +!> https://github.com/samharrison7/fortran-error-handler +module m_result_dp + + use kind_parameters, only: dp + use m_error_v, only: ErrorV + use m_result, only: ResultBase + use m_result_none, only: ResultNone + + implicit none (type, external) + private + + type, extends(ResultBase), public :: ResultDP + !! Result type that holds integer values + + real(kind=dp), allocatable :: data_v + !! Data i.e. the result (if no error occurs) + + ! Note: the error_v attribute comes from ResultBase + + contains + + private + + procedure, public :: build + procedure, public :: finalise + final :: finalise_auto + + end type ResultDP + + interface ResultDP + !! Constructor interface - see build [TODO: x-ref] for details + module procedure :: constructor + end interface ResultDP + +contains + + function constructor(data_v, error_v) result(self) + !! Build instance + + type(ResultDP) :: self + ! Hopefully can leave without docstring (like Python) + + real(kind=dp), intent(in), optional :: data_v + !! Data + + class(ErrorV), intent(in), optional :: error_v + !! Error + + type(ResultNone) :: build_res + + build_res = self % build(data_v_in=data_v, error_v_in=error_v) + + if (build_res % is_error()) then + + ! This interface has to return the initialised object, + ! it cannot return a Result type, + ! so we have no choice but to raise a fatal error here. + print *, build_res % error_v % message + error stop build_res % error_v % code + + ! else + ! Assume no error occurred and initialisation was fine + + end if + + end function constructor + + function build(self, data_v_in, error_v_in) result(res) + !! Build instance + + class(ResultDP), intent(out) :: self + ! Hopefully can leave without docstring (like Python) + + real(kind=dp), intent(in), optional :: data_v_in + !! Data + + class(ErrorV), intent(in), optional :: error_v_in + !! Error message + + type(ResultNone) :: res + !! Result + + if (present(data_v_in) .and. present(error_v_in)) then + res % error_v % message = "Both data and error were provided" + + else if (present(data_v_in)) then + allocate (self % data_v, source=data_v_in) + ! No error - no need to call res % build + + else if (present(error_v_in)) then + allocate (self % error_v, source=error_v_in) + ! No error - no need to call res % build + + else + res % error_v % message = "Neither data nor error were provided" + + end if + + end function build + + subroutine finalise(self) + !! Finalise the instance (i.e. free/deallocate) + + class(ResultDP), intent(inout) :: self + ! Hopefully can leave without docstring (like Python) + + if (allocated(self % data_v)) deallocate (self % data_v) + if (allocated(self % error_v)) deallocate(self % error_v) + + end subroutine finalise + + subroutine finalise_auto(self) + !! Finalise the instance (i.e. free/deallocate) + !! + !! This method is expected to be called automatically + !! by clever clean up, which is why it differs from [TODO x-ref] `finalise` + + type(ResultDP), intent(inout) :: self + ! Hopefully can leave without docstring (like Python) + + call self % finalise() + + end subroutine finalise_auto + +end module m_result_dp diff --git a/tests/unit/test_error_v_creation.py b/tests/unit/test_error_v_creation.py index e272d13..6d19637 100644 --- a/tests/unit/test_error_v_creation.py +++ b/tests/unit/test_error_v_creation.py @@ -29,6 +29,7 @@ def test_create_error_even(): assert res.message == "Even number supplied" +@pytest.mark.xfail(reason="Not implemented") def test_create_error_negative_raises(): # TODO: switch to more precise error type with pytest.raises(FortranError): From 4b6f7286aff9cddabc14027177e5b6bd63c4043f Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 11 Sep 2025 15:50:50 +0200 Subject: [PATCH 03/11] Add failing tests --- tests/unit/test_get_square_root.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/unit/test_get_square_root.py diff --git a/tests/unit/test_get_square_root.py b/tests/unit/test_get_square_root.py new file mode 100644 index 0000000..ac36f16 --- /dev/null +++ b/tests/unit/test_get_square_root.py @@ -0,0 +1,27 @@ +""" +Tests of `example_fgen_basic.get_square_root` +""" + +import pytest + +from example_fgen_basic.get_square_root import get_square_root +from example_fgen_basic.pyfgen_runtime.exceptions import FortranError + + +@pytest.mark.parametrize( + "inv, exp, exp_error", + ( + (4.0, 2.0, None), + (-4.0, None, pytest.raises(FortranError, match="inv is negative")), + ), +) +def test_basic(inv, exp, exp_error): + if exp is not None: + assert get_square_root(inv) == exp + + else: + if exp_error is None: + raise AssertionError + + with exp_error: + get_square_root(inv) From 053fafda9ba920448056ce98416ff3af45a7c310 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 11 Sep 2025 16:05:08 +0200 Subject: [PATCH 04/11] Up to writing wrapper --- meson.build | 3 + src/example_fgen_basic/error_v/creation.py | 4 - src/example_fgen_basic/error_v/error_v.py | 4 + src/example_fgen_basic/get_square_root.py | 61 ++++++++++++++ src/example_fgen_basic/result/__init__.py | 7 ++ src/example_fgen_basic/result/result_dp.py | 92 ++++++++++++++++++++++ 6 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 src/example_fgen_basic/get_square_root.py create mode 100644 src/example_fgen_basic/result/__init__.py create mode 100644 src/example_fgen_basic/result/result_dp.py diff --git a/meson.build b/meson.build index bc1b5c3..3f7a996 100644 --- a/meson.build +++ b/meson.build @@ -85,9 +85,12 @@ if pyprojectwheelbuild_enabled 'src/example_fgen_basic/error_v/error_v.py', 'src/example_fgen_basic/error_v/passing.py', 'src/example_fgen_basic/exceptions.py', + 'src/example_fgen_basic/get_square_root.py', 'src/example_fgen_basic/get_wavelength.py', 'src/example_fgen_basic/pyfgen_runtime/__init__.py', 'src/example_fgen_basic/pyfgen_runtime/exceptions.py', + 'src/example_fgen_basic/result/__init__.py', + 'src/example_fgen_basic/result/result_dp.py', 'src/example_fgen_basic/typing.py', ) diff --git a/src/example_fgen_basic/error_v/creation.py b/src/example_fgen_basic/error_v/creation.py index a0695d6..39d9c45 100644 --- a/src/example_fgen_basic/error_v/creation.py +++ b/src/example_fgen_basic/error_v/creation.py @@ -54,10 +54,6 @@ def create_error(inv: int) -> ErrorV: # Initialise the result from the received index res = ErrorV.from_instance_index(instance_index) - # Tell Fortran to finalise the object on the Fortran side - # (all data has been copied to Python now) - m_error_v_w.finalise_instance(instance_index) - return res diff --git a/src/example_fgen_basic/error_v/error_v.py b/src/example_fgen_basic/error_v/error_v.py index c508148..0743e2f 100644 --- a/src/example_fgen_basic/error_v/error_v.py +++ b/src/example_fgen_basic/error_v/error_v.py @@ -59,6 +59,10 @@ def from_instance_index(cls, instance_index: int) -> ErrorV: res = cls(code=code, message=message) + # Tell Fortran to finalise the object on the Fortran side + # (all data has been copied to Python now) + m_error_v_w.finalise_instance(instance_index) + return res def build_fortran_instance(self) -> int: diff --git a/src/example_fgen_basic/get_square_root.py b/src/example_fgen_basic/get_square_root.py new file mode 100644 index 0000000..c03328b --- /dev/null +++ b/src/example_fgen_basic/get_square_root.py @@ -0,0 +1,61 @@ +""" +Get square root of a number +""" + +from __future__ import annotations + +from example_fgen_basic.pyfgen_runtime.exceptions import ( + CompiledExtensionNotFoundError, + FortranError, +) +from example_fgen_basic.result import ResultDP + +try: + from example_fgen_basic._lib import m_get_square_root_w # type: ignore +except (ModuleNotFoundError, ImportError) as exc: # pragma: no cover + raise CompiledExtensionNotFoundError( + "example_fgen_basic._lib.m_get_square_root_w" + ) from exc + +try: + from example_fgen_basic._lib import m_result_dp_w # type: ignore +except (ModuleNotFoundError, ImportError) as exc: # pragma: no cover + raise CompiledExtensionNotFoundError( + "example_fgen_basic._lib.m_result_dp_w" + ) from exc + + +def get_square_root(inv: float) -> float: + """ + Get square root + + Parameters + ---------- + inv + Value for which to get the square root + + Returns + ------- + : + Square root of `inv` + + Raises + ------ + FortranError + `inv` is negative + + TODO: use a more specific error + """ + result_instance_index: int = m_get_square_root_w.get_wavelength(inv) + + result = ResultDP.from_instance_index(result_instance_index) + + if result.is_error: + # TODO: be more specific + raise FortranError(result.error_v.message) + + res = result.data_v + + m_result_dp_w.finalise_instance(result_instance_index) + + return res diff --git a/src/example_fgen_basic/result/__init__.py b/src/example_fgen_basic/result/__init__.py new file mode 100644 index 0000000..a0d7592 --- /dev/null +++ b/src/example_fgen_basic/result/__init__.py @@ -0,0 +1,7 @@ +""" +Definition of result values +""" + +from example_fgen_basic.result.result_dp import ResultDP + +__all__ = ["ResultDP"] diff --git a/src/example_fgen_basic/result/result_dp.py b/src/example_fgen_basic/result/result_dp.py new file mode 100644 index 0000000..a928b5f --- /dev/null +++ b/src/example_fgen_basic/result/result_dp.py @@ -0,0 +1,92 @@ +""" +Python equivalent of the Fortran `ResultDP` class [TODO: x-refs] +""" + +from __future__ import annotations + +from attrs import define + +from example_fgen_basic.error_v import ErrorV +from example_fgen_basic.pyfgen_runtime.exceptions import CompiledExtensionNotFoundError + +try: + from example_fgen_basic._lib import ( # type: ignore + m_result_dp_w, + ) +except (ModuleNotFoundError, ImportError) as exc: # pragma: no cover + raise CompiledExtensionNotFoundError( + "example_fgen_basic._lib.m_result_dp_w" + ) from exc + + +@define +class ResultDP: + """ + Result type that can hold double precision real values + """ + + # TODO: add validation that one of data_v and error_v is provided but not both + + # data_v: np.Float64 + data_v: float + """Data""" + + error_v: ErrorV + """Error""" + + @classmethod + def from_instance_index(cls, instance_index: int) -> ErrorV: + """ + Initialise from an instance index received from Fortran + + Parameters + ---------- + instance_index + Instance index received from Fortran + + Returns + ------- + : + Initialised index + """ + # Different wrapping strategies are needed + + # Integer is very simple + if m_result_dp_w.data_v_is_set(instance_index): + data_v = m_result_dp_w.get_data_v(instance_index) + + else: + data_v = None + + # Error type requires derived type handling + if m_result_dp_w.error_v_is_set(instance_index): + error_v_instance_index: int = m_result_dp_w.get_error_v(instance_index) + + # Initialise the result from the received index + error_v = ErrorV.from_instance_index(error_v_instance_index) + + else: + error_v = None + + res = cls(data_v=data_v, error_v=error_v) + + return res + + def build_fortran_instance(self) -> int: + """ + Build an instance equivalent to `self` on the Fortran side + + Intended for use mainly by wrapping functions. + Most users should not need to use this method directly. + + Returns + ------- + : + Instance index of the object which has been created on the Fortran side + """ + raise NotImplementedError + # instance_index: int = m_error_v_w.build_instance( + # code=self.code, message=self.message + # ) + # + # return instance_index From 61ba5dc97b46d4eb8af68a1ea0b51c392af9bca5 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 11 Sep 2025 16:28:54 +0200 Subject: [PATCH 05/11] Get test failing --- meson.build | 3 + src/example_fgen_basic/get_square_root.py | 4 +- .../get_square_root_wrapper.f90 | 47 +++++ src/example_fgen_basic/result/result_dp.py | 15 +- .../result/result_dp_manager.f90 | 188 ++++++++++++++++++ .../result/result_dp_wrapper.f90 | 178 +++++++++++++++++ 6 files changed, 432 insertions(+), 3 deletions(-) create mode 100644 src/example_fgen_basic/get_square_root_wrapper.f90 create mode 100644 src/example_fgen_basic/result/result_dp_manager.f90 create mode 100644 src/example_fgen_basic/result/result_dp_wrapper.f90 diff --git a/meson.build b/meson.build index 3f7a996..c996568 100644 --- a/meson.build +++ b/meson.build @@ -54,7 +54,9 @@ if pyprojectwheelbuild_enabled 'src/example_fgen_basic/error_v/creation_wrapper.f90', 'src/example_fgen_basic/error_v/error_v_wrapper.f90', 'src/example_fgen_basic/error_v/passing_wrapper.f90', + 'src/example_fgen_basic/get_square_root_wrapper.f90', 'src/example_fgen_basic/get_wavelength_wrapper.f90', + 'src/example_fgen_basic/result/result_dp_wrapper.f90', ) # Specify all the other source Fortran files (original files and managers) @@ -71,6 +73,7 @@ if pyprojectwheelbuild_enabled 'src/example_fgen_basic/kind_parameters.f90', 'src/example_fgen_basic/result/result.f90', 'src/example_fgen_basic/result/result_dp.f90', + 'src/example_fgen_basic/result/result_dp_manager.f90', 'src/example_fgen_basic/result/result_int.f90', 'src/example_fgen_basic/result/result_int1D.f90', 'src/example_fgen_basic/result/result_none.f90', diff --git a/src/example_fgen_basic/get_square_root.py b/src/example_fgen_basic/get_square_root.py index c03328b..4179bbc 100644 --- a/src/example_fgen_basic/get_square_root.py +++ b/src/example_fgen_basic/get_square_root.py @@ -46,11 +46,11 @@ def get_square_root(inv: float) -> float: TODO: use a more specific error """ - result_instance_index: int = m_get_square_root_w.get_wavelength(inv) + result_instance_index: int = m_get_square_root_w.get_square_root(inv) result = ResultDP.from_instance_index(result_instance_index) - if result.is_error: + if result.has_error: # TODO: be more specific raise FortranError(result.error_v.message) diff --git a/src/example_fgen_basic/get_square_root_wrapper.f90 b/src/example_fgen_basic/get_square_root_wrapper.f90 new file mode 100644 index 0000000..6b43362 --- /dev/null +++ b/src/example_fgen_basic/get_square_root_wrapper.f90 @@ -0,0 +1,47 @@ +!> Wrapper for interfacing `m_get_square_root` with python +module m_get_square_root_w + + use m_result_dp, only: ResultDP + use m_get_square_root, only: o_get_square_root => get_square_root + + ! The manager module, which makes this all work + use m_result_dp_manager, only: & + result_dp_manager_get_available_instance_index => get_available_instance_index, & + result_dp_manager_set_instance_index_to => set_instance_index_to, & + result_dp_manager_ensure_instance_array_size_is_at_least => ensure_instance_array_size_is_at_least + + implicit none(type, external) + private + + public :: get_square_root + +contains + + function get_square_root(inv) result(res_instance_index) + + ! Annoying that this has to be injected everywhere, + ! but ok it can be automated. + integer, parameter :: dp = selected_real_kind(15, 307) + + real(kind=dp), intent(in) :: inv + !! inv + + integer :: res_instance_index + !! Instance index of the result type + + type(ResultDP) :: res + + res = o_get_square_root(inv) + + call result_dp_manager_ensure_instance_array_size_is_at_least(1) + + ! Get the instance index to return to Python + call result_dp_manager_get_available_instance_index(res_instance_index) + + ! Set the derived type value in the manager's array, + ! ready for its attributes to be retrieved from Python. + call result_dp_manager_set_instance_index_to(res_instance_index, res) + + end function get_square_root + +end module m_get_square_root_w diff --git a/src/example_fgen_basic/result/result_dp.py b/src/example_fgen_basic/result/result_dp.py index a928b5f..5fb6fa8 100644 --- a/src/example_fgen_basic/result/result_dp.py +++ b/src/example_fgen_basic/result/result_dp.py @@ -53,7 +53,8 @@ def from_instance_index(cls, instance_index: int) -> ErrorV: # Integer is very simple if m_result_dp_w.data_v_is_set(instance_index): - data_v = m_result_dp_w.get_data_v(instance_index) + data_v: float = m_result_dp_w.get_data_v(instance_index) + # data_v: np.Float64 = m_result_dp_w.get_data_v(instance_index) else: data_v = None @@ -72,6 +73,18 @@ def from_instance_index(cls, instance_index: int) -> ErrorV: return res + @property + def has_error(self) -> bool: + """ + Whether this instance holds an error or not + + Returns + ------- + : + `True` if this instance holds an error, `False` otherwise + """ + return self.error_v is not None + def build_fortran_instance(self) -> int: """ Build an instance equivalent to `self` on the Fortran side diff --git a/src/example_fgen_basic/result/result_dp_manager.f90 b/src/example_fgen_basic/result/result_dp_manager.f90 new file mode 100644 index 0000000..796dbc7 --- /dev/null +++ b/src/example_fgen_basic/result/result_dp_manager.f90 @@ -0,0 +1,188 @@ +!> Manager of `ResultDP` (TODO: xref) across the Fortran-Python interface +module m_result_dp_manager + + use kind_parameters, only: dp + use m_error_v, only: ErrorV + use m_result_dp, only: ResultDP + use m_result_none, only: ResultNone + + implicit none(type, external) + private + + type(ResultDP), dimension(:), allocatable :: instance_array + logical, dimension(:), allocatable :: instance_available + + ! TODO: think about ordering here, alphabetical probably easiest + public :: build_instance, finalise_instance, get_available_instance_index, get_instance, set_instance_index_to, & + ensure_instance_array_size_is_at_least + +contains + + function build_instance(data_v_in, error_v_in) result(instance_index) + !! Build an instance + + real(kind=dp), intent(in), optional :: data_v_in + !! Data + + class(ErrorV), intent(in), optional :: error_v_in + !! Error message + + integer :: instance_index + !! Index of the built instance + + type(ResultNone) :: res_build + + call ensure_instance_array_size_is_at_least(1) + call get_available_instance_index(instance_index) + res_build = instance_array(instance_index) % build(data_v_in=data_v_in, error_v_in=error_v_in) + + ! TODO: check build has no error + + end function build_instance + + subroutine finalise_instance(instance_index) + !! Finalise an instance + + integer, intent(in) :: instance_index + !! Index of the instance to finalise + + call check_index_claimed(instance_index) + + call instance_array(instance_index) % finalise() + instance_available(instance_index) = .true. + + end subroutine finalise_instance + + subroutine get_available_instance_index(available_instance_index) + !! Get a free instance index + + ! TODO: think through whether race conditions are possible + ! e.g. while returning a free index number to one Python call + ! a different one can be looking up a free instance index at the same time + ! and something goes wrong (maybe we need a lock) + + integer, intent(out) :: available_instance_index + !! Available instance index + + integer :: i + + do i = 1, size(instance_array) + + if (instance_available(i)) then + + instance_available(i) = .false. + available_instance_index = i + ! TODO: switch to returning a Result type + ! res = ResultInt(data=i) + return + + end if + + end do + + ! TODO: switch to returning a Result type with an error set + ! res = ResultInt(ResultDP(code=1, message="No available instances")) + error stop 1 + + end subroutine get_available_instance_index + + ! Change to pure function when we update check_index_claimed to be pure + function get_instance(instance_index) result(inst) + + integer, intent(in) :: instance_index + !! Index in `instance_array` of which to set the value equal to `val` + + type(ResultDP) :: inst + !! Instance at `instance_array(instance_index)` + + call check_index_claimed(instance_index) + inst = instance_array(instance_index) + + end function get_instance + + subroutine set_instance_index_to(instance_index, val) + + integer, intent(in) :: instance_index + !! Index in `instance_array` of which to set the value equal to `val` + + type(ResultDP), intent(in) :: val + + call check_index_claimed(instance_index) + instance_array(instance_index) = val + + end subroutine set_instance_index_to + + subroutine check_index_claimed(instance_index) + !! Check that an index has already been claimed + !! + !! Stops execution if the index has not been claimed. + + integer, intent(in) :: instance_index + !! Instance index to check + + if (instance_available(instance_index)) then + ! TODO: Switch to using Result here + ! Use `ResultNone` which is a Result type + ! that doesn't have a `data` attribute + ! (i.e. if this succeeds, there is no data to check, + ! if it fails, the result_dp attribute will be set). + ! So the code would be something like + ! res = ResultNone(ResultDP(code=1, message="Index ", instance_index, " has not been claimed")) + print *, "Index ", instance_index, " has not been claimed" + error stop 1 + end if + + if (instance_index < 1) then + ! TODO: Switch to using Result here + ! Use `ResultNone` which is a Result type + ! that doesn't have a `data` attribute + ! (i.e. if this succeeds, there is no data to check, + ! if it fails, the result_dp attribute will be set). + ! So the code would be something like + ! res = ResultNone(ResultDP(code=2, message="Requested index is ", instance_index, " which is less than 1")) + print *, "Requested index is ", instance_index, " which is less than 1" + error stop 1 + end if + + ! ! Here, result becomes + ! ! Now that I've thought about this, it's also clear + ! ! that we will only use functions + ! ! or subroutines with a result type that has `intent(out)`. + ! ! We will no longer have subroutines that return nothing + ! ! (like this one currently does). + ! res = ResultNone() + + end subroutine check_index_claimed + + subroutine ensure_instance_array_size_is_at_least(n) + !! Ensure that `instance_array` and `instance_available` have at least `n` slots + + integer, intent(in) :: n + + type(ResultDP), dimension(:), allocatable :: tmp_instances + logical, dimension(:), allocatable :: tmp_available + + if (.not. allocated(instance_array)) then + + allocate (instance_array(n)) + + allocate (instance_available(n)) + ! Race conditions ? + instance_available = .true. + + else if (size(instance_available) < n) then + + allocate (tmp_instances(n)) + tmp_instances(1:size(instance_array)) = instance_array + call move_alloc(tmp_instances, instance_array) + + allocate (tmp_available(n)) + tmp_available(1:size(instance_available)) = instance_available + tmp_available(size(instance_available) + 1:size(tmp_available)) = .true. + call move_alloc(tmp_available, instance_available) + + end if + + end subroutine ensure_instance_array_size_is_at_least + +end module m_result_dp_manager diff --git a/src/example_fgen_basic/result/result_dp_wrapper.f90 b/src/example_fgen_basic/result/result_dp_wrapper.f90 new file mode 100644 index 0000000..958c437 --- /dev/null +++ b/src/example_fgen_basic/result/result_dp_wrapper.f90 @@ -0,0 +1,178 @@ +!> Wrapper for interfacing `m_result_dp` with Python +module m_result_dp_w + + use kind_parameters, only: dp + use m_error_v, only: ErrorV + use m_result_dp, only: ResultDP + + ! The manager module, which makes this all work + use m_error_v_manager, only: & + error_v_manager_get_instance => get_instance, & + error_v_manager_get_available_instance_index => get_available_instance_index, & + error_v_manager_set_instance_index_to => set_instance_index_to + + use m_result_dp_manager, only: & + result_dp_manager_build_instance => build_instance, & + result_dp_manager_finalise_instance => finalise_instance, & + result_dp_manager_get_instance => get_instance, & + result_dp_manager_ensure_instance_array_size_is_at_least => ensure_instance_array_size_is_at_least + + implicit none(type, external) + private + + public :: build_instance, finalise_instance, finalise_instances, & + ensure_at_least_n_instances_can_be_passed_simultaneously, & + data_v_is_set, get_data_v, error_v_is_set, get_error_v + +contains + + subroutine build_instance(data_v, error_v_instance_index, instance_index) + !! Build an instance + + real(kind=dp), intent(in), optional :: data_v + !! Data + + integer, intent(in), optional :: error_v_instance_index + !! Error + + integer, intent(out) :: instance_index + !! Instance index of the built instance + ! + ! This is the major trick for wrapping. + ! We pass instance indexes (integers) to Python rather than the instance itself. + + ! This is the major trick for wrapping derived types with other derived types as attributes. + ! We use the manager layer to initialise the attributes before passing on. + type(ErrorV) :: error_v + + error_v = error_v_manager_get_instance(error_v_instance_index) + + instance_index = result_dp_manager_build_instance(data_v, error_v) + + end subroutine build_instance + + ! build_instances is very hard to do + ! because you need to pass an array of variable-length characters which is non-trivial. + ! Maybe we will try this another day, for now this isn't that important + ! (we can just use a loop from the Python side) + ! so we just don't bother implementing `build_instances`. + + subroutine finalise_instance(instance_index) + !! Finalise an instance + + integer, intent(in) :: instance_index + !! Instance index + ! + ! This is the major trick for wrapping. + ! We pass instance indexes (integers) to Python rather than the instance itself. + + call result_dp_manager_finalise_instance(instance_index) + + end subroutine finalise_instance + + subroutine finalise_instances(instance_indexes) + !! Finalise an instance + + integer, dimension(:), intent(in) :: instance_indexes + !! Instance indexes to finalise + ! + ! This is the major trick for wrapping. + ! We pass instance indexes (integers) to Python rather than the instance itself. + + integer :: i + + do i = 1, size(instance_indexes) + call result_dp_manager_finalise_instance(instance_indexes(i)) + end do + + end subroutine finalise_instances + + subroutine ensure_at_least_n_instances_can_be_passed_simultaneously(n) + !! Ensure that at least `n` instances of `ResultDP` can be passed via the manager simultaneously + + integer, intent(in) :: n + + call result_dp_manager_ensure_instance_array_size_is_at_least(n) + + end subroutine ensure_at_least_n_instances_can_be_passed_simultaneously + + ! Full set of wrapping strategies to get/pass different types in e.g. + ! https://gitlab.com/magicc/fgen/-/blob/switch-to-uv/tests/test-data/exposed_attrs/src/exposed_attrs/exposed_attrs_wrapped.f90 + ! (we will do a full re-write of the code which generates this, + ! but the strategies will probably stay as they are) + + ! For optional stuff, need to be able to check whether they're set or not + subroutine data_v_is_set( & + instance_index, & + res & + ) + + integer, intent(in) :: instance_index + + logical, intent(out) :: res + + type(ResultDP) :: instance + + instance = result_dp_manager_get_instance(instance_index) + + res = allocated(instance % data_v) + + end subroutine data_v_is_set + + subroutine get_data_v( & + instance_index, & + data_v & + ) + + integer, intent(in) :: instance_index + + real(kind=dp), intent(out) :: data_v + + type(ResultDP) :: instance + + instance = result_dp_manager_get_instance(instance_index) + + data_v = instance % data_v + + end subroutine get_data_v + + subroutine error_v_is_set( & + instance_index, & + res & + ) + + integer, intent(in) :: instance_index + + logical, intent(out) :: res + + type(ResultDP) :: instance + + instance = result_dp_manager_get_instance(instance_index) + + res = allocated(instance % error_v) + + end subroutine error_v_is_set + + subroutine get_error_v( & + instance_index, & + error_v_instance_index & + ) + + integer, intent(in) :: instance_index + + ! trick: return instance index, not the instance. + ! Build on the python side + integer, intent(out) :: error_v_instance_index + + type(ResultDP) :: instance + type(ErrorV) :: error_v + + instance = result_dp_manager_get_instance(instance_index) + + error_v = instance % error_v + call error_v_manager_get_available_instance_index(error_v_instance_index) + call error_v_manager_set_instance_index_to(error_v_instance_index, error_v) + + end subroutine get_error_v + +end module m_result_dp_w From 9dc7b3fe538d35f6af07147bffdbe64cf2bcbf97 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 11 Sep 2025 16:34:06 +0200 Subject: [PATCH 06/11] Get one test passing --- src/example_fgen_basic/get_square_root_wrapper.f90 | 5 +++++ src/example_fgen_basic/result/result_dp_wrapper.f90 | 13 ++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/example_fgen_basic/get_square_root_wrapper.f90 b/src/example_fgen_basic/get_square_root_wrapper.f90 index 6b43362..f325ea1 100644 --- a/src/example_fgen_basic/get_square_root_wrapper.f90 +++ b/src/example_fgen_basic/get_square_root_wrapper.f90 @@ -42,6 +42,11 @@ function get_square_root(inv) result(res_instance_index) ! ready for its attributes to be retrieved from Python. call result_dp_manager_set_instance_index_to(res_instance_index, res) + print *, "res_instance_index" + print *, res_instance_index + print *, "res % data_v" + print *, res % data_v + end function get_square_root end module m_get_square_root_w diff --git a/src/example_fgen_basic/result/result_dp_wrapper.f90 b/src/example_fgen_basic/result/result_dp_wrapper.f90 index 958c437..4312461 100644 --- a/src/example_fgen_basic/result/result_dp_wrapper.f90 +++ b/src/example_fgen_basic/result/result_dp_wrapper.f90 @@ -1,7 +1,6 @@ !> Wrapper for interfacing `m_result_dp` with Python module m_result_dp_w - use kind_parameters, only: dp use m_error_v, only: ErrorV use m_result_dp, only: ResultDP @@ -29,6 +28,10 @@ module m_result_dp_w subroutine build_instance(data_v, error_v_instance_index, instance_index) !! Build an instance + ! Annoying that this has to be injected everywhere, + ! but ok it can be automated. + integer, parameter :: dp = selected_real_kind(15, 307) + real(kind=dp), intent(in), optional :: data_v !! Data @@ -124,15 +127,23 @@ subroutine get_data_v( & data_v & ) + ! Annoying that this has to be injected everywhere, + ! but ok it can be automated. + integer, parameter :: dp = selected_real_kind(15, 307) + integer, intent(in) :: instance_index real(kind=dp), intent(out) :: data_v type(ResultDP) :: instance + print *, "instance_index" + print *, instance_index instance = result_dp_manager_get_instance(instance_index) data_v = instance % data_v + print *, "instance % data_v" + print *, instance % data_v end subroutine get_data_v From 31ebe4c98af6b162a1c3667846e3f1fe28798046 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 11 Sep 2025 16:46:35 +0200 Subject: [PATCH 07/11] Pass error raising test --- src/example_fgen_basic/get_square_root.py | 10 +++++++++- src/example_fgen_basic/get_square_root_wrapper.f90 | 5 ----- src/example_fgen_basic/result/result_dp_wrapper.f90 | 7 +++---- tests/unit/test_get_square_root.py | 2 +- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/example_fgen_basic/get_square_root.py b/src/example_fgen_basic/get_square_root.py index 4179bbc..61bf638 100644 --- a/src/example_fgen_basic/get_square_root.py +++ b/src/example_fgen_basic/get_square_root.py @@ -47,15 +47,23 @@ def get_square_root(inv: float) -> float: TODO: use a more specific error """ result_instance_index: int = m_get_square_root_w.get_square_root(inv) - result = ResultDP.from_instance_index(result_instance_index) if result.has_error: # TODO: be more specific raise FortranError(result.error_v.message) + # raise LessThanZeroError(result.error_v.message) res = result.data_v + # TODO: think + # I like the clarity of finalising result_instance_index here + # by having an explicit call + # (so you can see creation and finalisation in same place). + # (Probably the above is my preferred right now, but we should think about it.) + # I like the safety of finalising in `from_instance_index`. + # if not finalised(result_instance_index): + # finalise(result_instance_index) m_result_dp_w.finalise_instance(result_instance_index) return res diff --git a/src/example_fgen_basic/get_square_root_wrapper.f90 b/src/example_fgen_basic/get_square_root_wrapper.f90 index f325ea1..6b43362 100644 --- a/src/example_fgen_basic/get_square_root_wrapper.f90 +++ b/src/example_fgen_basic/get_square_root_wrapper.f90 @@ -42,11 +42,6 @@ function get_square_root(inv) result(res_instance_index) ! ready for its attributes to be retrieved from Python. call result_dp_manager_set_instance_index_to(res_instance_index, res) - print *, "res_instance_index" - print *, res_instance_index - print *, "res % data_v" - print *, res % data_v - end function get_square_root end module m_get_square_root_w diff --git a/src/example_fgen_basic/result/result_dp_wrapper.f90 b/src/example_fgen_basic/result/result_dp_wrapper.f90 index 4312461..1625f7e 100644 --- a/src/example_fgen_basic/result/result_dp_wrapper.f90 +++ b/src/example_fgen_basic/result/result_dp_wrapper.f90 @@ -7,6 +7,7 @@ module m_result_dp_w ! The manager module, which makes this all work use m_error_v_manager, only: & error_v_manager_get_instance => get_instance, & + error_v_manager_ensure_instance_array_size_is_at_least => ensure_instance_array_size_is_at_least, & error_v_manager_get_available_instance_index => get_available_instance_index, & error_v_manager_set_instance_index_to => set_instance_index_to @@ -137,13 +138,9 @@ subroutine get_data_v( & type(ResultDP) :: instance - print *, "instance_index" - print *, instance_index instance = result_dp_manager_get_instance(instance_index) data_v = instance % data_v - print *, "instance % data_v" - print *, instance % data_v end subroutine get_data_v @@ -181,6 +178,8 @@ subroutine get_error_v( & instance = result_dp_manager_get_instance(instance_index) error_v = instance % error_v + + call error_v_manager_ensure_instance_array_size_is_at_least(1) call error_v_manager_get_available_instance_index(error_v_instance_index) call error_v_manager_set_instance_index_to(error_v_instance_index, error_v) diff --git a/tests/unit/test_get_square_root.py b/tests/unit/test_get_square_root.py index ac36f16..4582961 100644 --- a/tests/unit/test_get_square_root.py +++ b/tests/unit/test_get_square_root.py @@ -12,7 +12,7 @@ "inv, exp, exp_error", ( (4.0, 2.0, None), - (-4.0, None, pytest.raises(FortranError, match="inv is negative")), + (-4.0, None, pytest.raises(FortranError, match="Input value was negative")), ), ) def test_basic(inv, exp, exp_error): From 0174330f176351e9afaf4d15517dd43bca38694d Mon Sep 17 00:00:00 2001 From: Marco Zecchetto Date: Fri, 12 Sep 2025 10:41:01 +0200 Subject: [PATCH 08/11] Skip test --- tests/unit/test_error_v_creation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_error_v_creation.py b/tests/unit/test_error_v_creation.py index 6d19637..2564d7d 100644 --- a/tests/unit/test_error_v_creation.py +++ b/tests/unit/test_error_v_creation.py @@ -29,7 +29,7 @@ def test_create_error_even(): assert res.message == "Even number supplied" -@pytest.mark.xfail(reason="Not implemented") +@pytest.mark.skip(reason="Not implemented") def test_create_error_negative_raises(): # TODO: switch to more precise error type with pytest.raises(FortranError): From f60b408f8fb48c532d73f03552faccde526448f6 Mon Sep 17 00:00:00 2001 From: Marco Zecchetto Date: Fri, 12 Sep 2025 11:39:12 +0200 Subject: [PATCH 09/11] Corrected create_errors small mistake --- src/example_fgen_basic/error_v/creation.py | 2 +- src/example_fgen_basic/meson.build | 1 + tests/unit/test_error_v_creation.f90 | 4 ++-- tests/unit/test_error_v_creation.py | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/example_fgen_basic/error_v/creation.py b/src/example_fgen_basic/error_v/creation.py index 39d9c45..92a14ae 100644 --- a/src/example_fgen_basic/error_v/creation.py +++ b/src/example_fgen_basic/error_v/creation.py @@ -57,7 +57,7 @@ def create_error(inv: int) -> ErrorV: return res -def create_errors(invs: NP_ARRAY_OF_INT) -> tuple[ErrorV, ...]: +def create_errors(invs: NP_ARRAY_OF_INT, n: int) -> tuple[ErrorV, ...]: """ Create a number of errors diff --git a/src/example_fgen_basic/meson.build b/src/example_fgen_basic/meson.build index 8c67049..b565a54 100644 --- a/src/example_fgen_basic/meson.build +++ b/src/example_fgen_basic/meson.build @@ -1,6 +1,7 @@ srcs += files( 'error_v/creation.f90', 'error_v/error_v.f90', + 'error_v/passing.f90', 'fpyfgen/base_finalisable.f90', 'get_wavelength.f90', 'kind_parameters.f90', diff --git a/tests/unit/test_error_v_creation.f90 b/tests/unit/test_error_v_creation.f90 index b5b8d85..fa1939f 100644 --- a/tests/unit/test_error_v_creation.f90 +++ b/tests/unit/test_error_v_creation.f90 @@ -27,7 +27,7 @@ end subroutine collect_error_v_creation_tests subroutine test_error_v_creation_basic(error) use m_error_v, only: ErrorV - use m_error_v_passing, only: create_error + use m_error_v_creation, only: create_error type(error_type), allocatable, intent(out) :: error @@ -46,7 +46,7 @@ end subroutine test_error_v_creation_basic subroutine test_error_v_creation_edge(error) use m_error_v, only: ErrorV - use m_error_v_passing, only: create_error + use m_error_v_creation, only: create_error type(error_type), allocatable, intent(out) :: error diff --git a/tests/unit/test_error_v_creation.py b/tests/unit/test_error_v_creation.py index 2564d7d..271ab5f 100644 --- a/tests/unit/test_error_v_creation.py +++ b/tests/unit/test_error_v_creation.py @@ -29,7 +29,7 @@ def test_create_error_even(): assert res.message == "Even number supplied" -@pytest.mark.skip(reason="Not implemented") +@pytest.mark.xfail(reason="Not implemented") def test_create_error_negative_raises(): # TODO: switch to more precise error type with pytest.raises(FortranError): @@ -48,7 +48,7 @@ def test_create_error_lots_of_repeated_calls(): def test_create_multiple_errors(): - res = create_errors(np.arange(6)) + res = create_errors(np.arange(6), 6) for i, v in enumerate(res): if i % 2 == 0: From 999f35fc185d2a04e6968737b203016320a3cdd1 Mon Sep 17 00:00:00 2001 From: Marco Zecchetto Date: Fri, 12 Sep 2025 13:11:10 +0200 Subject: [PATCH 10/11] Corrected error --- src/example_fgen_basic/error_v/creation.py | 10 ++++++++-- src/example_fgen_basic/error_v/error_v.py | 4 ---- src/example_fgen_basic/error_v/error_v_manager.f90 | 1 + tests/unit/test_error_v_creation.py | 6 +++--- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/example_fgen_basic/error_v/creation.py b/src/example_fgen_basic/error_v/creation.py index 92a14ae..b027682 100644 --- a/src/example_fgen_basic/error_v/creation.py +++ b/src/example_fgen_basic/error_v/creation.py @@ -54,10 +54,14 @@ def create_error(inv: int) -> ErrorV: # Initialise the result from the received index res = ErrorV.from_instance_index(instance_index) + # Tell Fortran to finalise the object on the Fortran side + # (all data has been copied to Python now) + m_error_v_w.finalise_instance(instance_index) + return res -def create_errors(invs: NP_ARRAY_OF_INT, n: int) -> tuple[ErrorV, ...]: +def create_errors(invs: NP_ARRAY_OF_INT) -> tuple[ErrorV, ...]: """ Create a number of errors @@ -76,7 +80,9 @@ def create_errors(invs: NP_ARRAY_OF_INT, n: int) -> tuple[ErrorV, ...]: Created errors """ # Get the result, but receiving an instance index rather than the object itself - instance_indexes: NP_ARRAY_OF_INT = m_error_v_creation_w.create_errors(invs) + instance_indexes: NP_ARRAY_OF_INT = m_error_v_creation_w.create_errors( + invs, len(invs) + ) # Initialise the result from the received index res = tuple(ErrorV.from_instance_index(i) for i in instance_indexes) diff --git a/src/example_fgen_basic/error_v/error_v.py b/src/example_fgen_basic/error_v/error_v.py index 0743e2f..c508148 100644 --- a/src/example_fgen_basic/error_v/error_v.py +++ b/src/example_fgen_basic/error_v/error_v.py @@ -59,10 +59,6 @@ def from_instance_index(cls, instance_index: int) -> ErrorV: res = cls(code=code, message=message) - # Tell Fortran to finalise the object on the Fortran side - # (all data has been copied to Python now) - m_error_v_w.finalise_instance(instance_index) - return res def build_fortran_instance(self) -> int: diff --git a/src/example_fgen_basic/error_v/error_v_manager.f90 b/src/example_fgen_basic/error_v/error_v_manager.f90 index 1546f12..7fc01d3 100644 --- a/src/example_fgen_basic/error_v/error_v_manager.f90 +++ b/src/example_fgen_basic/error_v/error_v_manager.f90 @@ -78,6 +78,7 @@ subroutine get_available_instance_index(available_instance_index) ! TODO: switch to returning a Result type with an error set ! res = ResultInt(ErrorV(code=1, message="No available instances")) + print *, "print" error stop 1 end subroutine get_available_instance_index diff --git a/tests/unit/test_error_v_creation.py b/tests/unit/test_error_v_creation.py index 271ab5f..6412234 100644 --- a/tests/unit/test_error_v_creation.py +++ b/tests/unit/test_error_v_creation.py @@ -23,7 +23,6 @@ def test_create_error_even(): res = create_error(2.0) assert isinstance(res, ErrorV) - assert res.code != 0 assert res.code == 1 assert res.message == "Even number supplied" @@ -48,12 +47,13 @@ def test_create_error_lots_of_repeated_calls(): def test_create_multiple_errors(): - res = create_errors(np.arange(6), 6) - + res = create_errors(np.arange(6)) for i, v in enumerate(res): if i % 2 == 0: + print(v.code, v.message) assert v.code == 1 assert v.message == "Even number supplied" else: + print(v.code, v.message) assert v.code == 0 assert v.message == "" From 3c2a6a49b9415f9073d17c5421c3f3d8fa5f94b7 Mon Sep 17 00:00:00 2001 From: Marco Zecchetto Date: Fri, 12 Sep 2025 13:21:50 +0200 Subject: [PATCH 11/11] Mypy --- src/example_fgen_basic/get_square_root.py | 7 +++++-- src/example_fgen_basic/result/result_dp.py | 8 ++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/example_fgen_basic/get_square_root.py b/src/example_fgen_basic/get_square_root.py index 61bf638..72715a2 100644 --- a/src/example_fgen_basic/get_square_root.py +++ b/src/example_fgen_basic/get_square_root.py @@ -18,7 +18,7 @@ ) from exc try: - from example_fgen_basic._lib import m_result_dp_w # type: ignore + from example_fgen_basic._lib import m_result_dp_w except (ModuleNotFoundError, ImportError) as exc: # pragma: no cover raise CompiledExtensionNotFoundError( "example_fgen_basic._lib.m_result_dp_w" @@ -49,11 +49,14 @@ def get_square_root(inv: float) -> float: result_instance_index: int = m_get_square_root_w.get_square_root(inv) result = ResultDP.from_instance_index(result_instance_index) - if result.has_error: + if result.error_v is not None: # TODO: be more specific raise FortranError(result.error_v.message) # raise LessThanZeroError(result.error_v.message) + if result.data_v is None: + raise AssertionError + res = result.data_v # TODO: think diff --git a/src/example_fgen_basic/result/result_dp.py b/src/example_fgen_basic/result/result_dp.py index 5fb6fa8..9e27bc7 100644 --- a/src/example_fgen_basic/result/result_dp.py +++ b/src/example_fgen_basic/result/result_dp.py @@ -28,14 +28,14 @@ class ResultDP: # TODO: add validation that one of data_v and error_v is provided but not both # data_v: np.Float64 - data_v: float + data_v: float | None """Data""" - error_v: ErrorV + error_v: ErrorV | None """Error""" @classmethod - def from_instance_index(cls, instance_index: int) -> ErrorV: + def from_instance_index(cls, instance_index: int) -> ResultDP: """ Initialise from an instance index received from Fortran @@ -53,7 +53,7 @@ def from_instance_index(cls, instance_index: int) -> ErrorV: # Integer is very simple if m_result_dp_w.data_v_is_set(instance_index): - data_v: float = m_result_dp_w.get_data_v(instance_index) + data_v: float | None = m_result_dp_w.get_data_v(instance_index) # data_v: np.Float64 = m_result_dp_w.get_data_v(instance_index) else: