From 76d942aecf0406b16b787c0f06cb35c3077ab9f6 Mon Sep 17 00:00:00 2001 From: Jared Lewis Date: Wed, 10 Apr 2024 23:02:42 +1000 Subject: [PATCH 1/3] chore: Update to fgen=0.4.1 --- .github/workflows/ci.yaml | 57 ++- cmake/Findfgen.cmake | 2 +- poetry.lock | 145 +----- pyproject.toml | 5 +- src/fgen_example/_lib/derived_type.yaml | 26 +- .../_lib/derived_type_manager.f90 | 90 ++-- .../_lib/derived_type_wrapped.f90 | 189 +++++--- src/fgen_example/_lib/operations.yaml | 22 +- src/fgen_example/_lib/operations_manager.f90 | 90 ++-- src/fgen_example/_lib/operations_wrapped.f90 | 166 ++++--- src/fgen_example/derived_type.py | 451 +++++++++++++++--- src/fgen_example/operations.py | 421 +++++++++++++--- tests/unit/test_wrapper.py | 22 +- 13 files changed, 1164 insertions(+), 522 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0eba1ef..858474c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -39,35 +39,34 @@ jobs: - name: docs run: poetry run sphinx-build -W --keep-going -T -b html docs/source docs/build -# Fix the tests in the next MR -# tests: -# strategy: -# fail-fast: false -# matrix: -# os: [ "ubuntu-latest" ] -# python-version: [ "3.10", "3.11", "3.12" ] -# runs-on: "${{ matrix.os }}" -# defaults: -# run: -# # This might be needed for Windows and doesn't seem to affect unix-based systems -# # so we include it. If you have better proof of whether this is needed or not, -# # feel free to update. -# shell: bash -# steps: -# - name: Check out repository -# uses: actions/checkout@v3 -# - uses: ./.github/actions/setup -# with: -# os: "${{ matrix.os }}" -# python-version: "${{ matrix.python-version }}" -# venv-id: "tests-${{ runner.os }}" -# poetry-dependency-install-flags: "--all-extras" -# - name: Run tests -# run: | -# poetry run pytest -r a -v src tests --doctest-modules --cov=src --cov-report=term-missing --cov-report=xml -# poetry run coverage report -# - name: Upload coverage reports to Codecov -# uses: codecov/codecov-action@v3 + tests: + strategy: + fail-fast: false + matrix: + os: [ "ubuntu-latest" ] + python-version: [ "3.10", "3.11", "3.12" ] + runs-on: "${{ matrix.os }}" + defaults: + run: + # This might be needed for Windows and doesn't seem to affect unix-based systems + # so we include it. If you have better proof of whether this is needed or not, + # feel free to update. + shell: bash + steps: + - name: Check out repository + uses: actions/checkout@v3 + - uses: ./.github/actions/setup + with: + os: "${{ matrix.os }}" + python-version: "${{ matrix.python-version }}" + venv-id: "tests-${{ runner.os }}" + poetry-dependency-install-flags: "--all-extras" + - name: Run tests + run: | + poetry run pytest -r a -v src tests --doctest-modules --cov=src --cov-report=term-missing --cov-report=xml + poetry run coverage report + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 imports-without-extras: strategy: diff --git a/cmake/Findfgen.cmake b/cmake/Findfgen.cmake index d683b6c..8e8f832 100644 --- a/cmake/Findfgen.cmake +++ b/cmake/Findfgen.cmake @@ -51,7 +51,7 @@ The following cache variables may be set to influence the library detection: set(_lib "fgen") set(_pkg "FGEN") set(_url "https://gitlab.com/magicc/fgen") -set(_hash "7068f0ec5cd028977c6952434d909e40a729d91e") # v0.3.1 +set(_hash "5a0f96071addcae99a272cecb845770b533ae582") # v0.4.1 if(NOT DEFINED "${_pkg}_FIND_METHOD") if(DEFINED "${PROJECT_NAME}-dependency-method") diff --git a/poetry.lock b/poetry.lock index 5a2bc43..6f7c391 100644 --- a/poetry.lock +++ b/poetry.lock @@ -585,25 +585,27 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc [[package]] name = "fgen" -version = "0.3.1" +version = "0.4.1" description = "Automatically generate wrapper to integrate Fortran and Python" optional = false -python-versions = ">=3.9,<4.0" +python-versions = "<4.0,>=3.9" files = [ - {file = "fgen-0.3.1-py3-none-any.whl", hash = "sha256:dec3743b32baae2cf154bdb6ed6b39dc13adb3931f2181028a434daddd14ee17"}, - {file = "fgen-0.3.1.tar.gz", hash = "sha256:3317e6345bc1fffa69d6b1fe84816fae215d87ffe83019df2a2aefad4f39b481"}, + {file = "fgen-0.4.1-py3-none-any.whl", hash = "sha256:07c4ffc642b991a76d60cc0e96f2486c0bfad26829f5b43be0c756bc2b253c15"}, + {file = "fgen-0.4.1.tar.gz", hash = "sha256:0b31ab8558da514757c0107d63715d12788ab8b846fe57773a15348b27b5df5e"}, ] [package.dependencies] attrs = ">=23.0.0,<24.0.0" -black = ">=23.3.0,<24.0.0" cattrs = ">=23.0.0,<24.0.0" click = ">=8.0.0,<9.0.0" -cmakelang = ">=0.6.13,<0.7.0" -jinja2 = ">=3.1.2,<4.0.0" loguru = ">=0.7.2,<0.8.0" -openscm-units = ">=0.5.0,<0.6.0" +numpy = ">1.23" +pint = "*" pyyaml = ">=6.0,<7.0" +typing-extensions = ">=4.9.0,<5.0.0" + +[package.extras] +templates = ["black (>=23.3.0,<24.0.0)", "jinja2 (>=3.1.2,<4.0.0)"] [[package]] name = "filelock" @@ -621,17 +623,6 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1 testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] typing = ["typing-extensions (>=4.8)"] -[[package]] -name = "globalwarmingpotentials" -version = "0.9.4" -description = "Global warming potentials of greenhouse gases from various IPCC reports" -optional = false -python-versions = "*" -files = [ - {file = "globalwarmingpotentials-0.9.4-py2.py3-none-any.whl", hash = "sha256:3a64a5f49a9cd4b0d6a2be916871863de16a671daa152e0cd89adadbe2ab57c9"}, - {file = "globalwarmingpotentials-0.9.4.tar.gz", hash = "sha256:55738ff5fcda4bd506002e4c7bca46f94af03ec03544e9bfd64c369061fe72b2"}, -] - [[package]] name = "greenlet" version = "3.0.3" @@ -1448,25 +1439,6 @@ files = [ {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] -[[package]] -name = "openscm-units" -version = "0.5.4" -description = "Handling of units related to simple climate modelling." -optional = false -python-versions = ">=3.9,<4.0" -files = [ - {file = "openscm_units-0.5.4-py3-none-any.whl", hash = "sha256:6da46c2eaf37d1fc4df768050d6222c96d053b2c409c94cc41449a3ebffcaf93"}, - {file = "openscm_units-0.5.4.tar.gz", hash = "sha256:76429369e221084b6b41757369c536f852abc44679070f600ed72124fd58f9f9"}, -] - -[package.dependencies] -globalwarmingpotentials = "*" -pandas = "*" -pint = "*" - -[package.extras] -notebooks = ["notebook (>=7.0.0,<8.0.0)", "seaborn (>=0.13.0,<0.14.0)"] - [[package]] name = "packaging" version = "24.0" @@ -1478,79 +1450,6 @@ files = [ {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] -[[package]] -name = "pandas" -version = "2.2.1" -description = "Powerful data structures for data analysis, time series, and statistics" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pandas-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8df8612be9cd1c7797c93e1c5df861b2ddda0b48b08f2c3eaa0702cf88fb5f88"}, - {file = "pandas-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0f573ab277252ed9aaf38240f3b54cfc90fff8e5cab70411ee1d03f5d51f3944"}, - {file = "pandas-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f02a3a6c83df4026e55b63c1f06476c9aa3ed6af3d89b4f04ea656ccdaaaa359"}, - {file = "pandas-2.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c38ce92cb22a4bea4e3929429aa1067a454dcc9c335799af93ba9be21b6beb51"}, - {file = "pandas-2.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c2ce852e1cf2509a69e98358e8458775f89599566ac3775e70419b98615f4b06"}, - {file = "pandas-2.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53680dc9b2519cbf609c62db3ed7c0b499077c7fefda564e330286e619ff0dd9"}, - {file = "pandas-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:94e714a1cca63e4f5939cdce5f29ba8d415d85166be3441165edd427dc9f6bc0"}, - {file = "pandas-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f821213d48f4ab353d20ebc24e4faf94ba40d76680642fb7ce2ea31a3ad94f9b"}, - {file = "pandas-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70e00c2d894cb230e5c15e4b1e1e6b2b478e09cf27cc593a11ef955b9ecc81a"}, - {file = "pandas-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97fbb5387c69209f134893abc788a6486dbf2f9e511070ca05eed4b930b1b02"}, - {file = "pandas-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101d0eb9c5361aa0146f500773395a03839a5e6ecde4d4b6ced88b7e5a1a6403"}, - {file = "pandas-2.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7d2ed41c319c9fb4fd454fe25372028dfa417aacb9790f68171b2e3f06eae8cd"}, - {file = "pandas-2.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:af5d3c00557d657c8773ef9ee702c61dd13b9d7426794c9dfeb1dc4a0bf0ebc7"}, - {file = "pandas-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:06cf591dbaefb6da9de8472535b185cba556d0ce2e6ed28e21d919704fef1a9e"}, - {file = "pandas-2.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:88ecb5c01bb9ca927ebc4098136038519aa5d66b44671861ffab754cae75102c"}, - {file = "pandas-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:04f6ec3baec203c13e3f8b139fb0f9f86cd8c0b94603ae3ae8ce9a422e9f5bee"}, - {file = "pandas-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a935a90a76c44fe170d01e90a3594beef9e9a6220021acfb26053d01426f7dc2"}, - {file = "pandas-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c391f594aae2fd9f679d419e9a4d5ba4bce5bb13f6a989195656e7dc4b95c8f0"}, - {file = "pandas-2.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9d1265545f579edf3f8f0cb6f89f234f5e44ba725a34d86535b1a1d38decbccc"}, - {file = "pandas-2.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11940e9e3056576ac3244baef2fedade891977bcc1cb7e5cc8f8cc7d603edc89"}, - {file = "pandas-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acf681325ee1c7f950d058b05a820441075b0dd9a2adf5c4835b9bc056bf4fb"}, - {file = "pandas-2.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9bd8a40f47080825af4317d0340c656744f2bfdb6819f818e6ba3cd24c0e1397"}, - {file = "pandas-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df0c37ebd19e11d089ceba66eba59a168242fc6b7155cba4ffffa6eccdfb8f16"}, - {file = "pandas-2.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:739cc70eaf17d57608639e74d63387b0d8594ce02f69e7a0b046f117974b3019"}, - {file = "pandas-2.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9d3558d263073ed95e46f4650becff0c5e1ffe0fc3a015de3c79283dfbdb3df"}, - {file = "pandas-2.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4aa1d8707812a658debf03824016bf5ea0d516afdea29b7dc14cf687bc4d4ec6"}, - {file = "pandas-2.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:76f27a809cda87e07f192f001d11adc2b930e93a2b0c4a236fde5429527423be"}, - {file = "pandas-2.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:1ba21b1d5c0e43416218db63037dbe1a01fc101dc6e6024bcad08123e48004ab"}, - {file = "pandas-2.2.1.tar.gz", hash = "sha256:0ab90f87093c13f3e8fa45b48ba9f39181046e8f3317d3aadb2fffbb1b978572"}, -] - -[package.dependencies] -numpy = [ - {version = ">=1.22.4,<2", markers = "python_version < \"3.11\""}, - {version = ">=1.23.2,<2", markers = "python_version == \"3.11\""}, - {version = ">=1.26.0,<2", markers = "python_version >= \"3.12\""}, -] -python-dateutil = ">=2.8.2" -pytz = ">=2020.1" -tzdata = ">=2022.7" - -[package.extras] -all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] -aws = ["s3fs (>=2022.11.0)"] -clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] -compression = ["zstandard (>=0.19.0)"] -computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] -consortium-standard = ["dataframe-api-compat (>=0.1.7)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] -feather = ["pyarrow (>=10.0.1)"] -fss = ["fsspec (>=2022.11.0)"] -gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] -hdf5 = ["tables (>=3.8.0)"] -html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] -mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] -parquet = ["pyarrow (>=10.0.1)"] -performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] -plot = ["matplotlib (>=3.6.3)"] -postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] -pyarrow = ["pyarrow (>=10.0.1)"] -spss = ["pyreadstat (>=1.2.0)"] -sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] -test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.9.2)"] - [[package]] name = "parso" version = "0.8.4" @@ -1810,17 +1709,6 @@ files = [ [package.dependencies] six = ">=1.5" -[[package]] -name = "pytz" -version = "2024.1" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, - {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, -] - [[package]] name = "pywin32" version = "306" @@ -2670,17 +2558,6 @@ files = [ {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, ] -[[package]] -name = "tzdata" -version = "2024.1" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -files = [ - {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, - {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, -] - [[package]] name = "urllib3" version = "2.2.1" @@ -2761,4 +2638,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "d73aed00834f18e80cfa9a50d155c4f88b8ebfe610ab60793e540cfd3773f015" +content-hash = "a2940525b091fe6373a6285b400399fdf122f7116f408a2f3fd0ba47a2615e6b" diff --git a/pyproject.toml b/pyproject.toml index 4280d47..319ce19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ packages = [{include = "fgen_example", from = "src"}] python = "^3.9" pint = "^0.22" numpy = "^1.25.2" -fgen = "0.3.1" +fgen = "0.4.1" [tool.poetry.group.tests.dependencies] pytest = "^7.3.1" @@ -49,7 +49,7 @@ liccheck = "^0.9.1" cmakelang = "^0.6.13" [build-system] -requires = ["scikit-build-core", "oldest-supported-numpy", "fgen==0.3.1"] +requires = ["scikit-build-core", "oldest-supported-numpy", "fgen[templates]==0.4.1"] build-backend = "scikit_build_core.build" [tool.scikit-build] @@ -135,6 +135,7 @@ ignore = [ "D105", # Missing docstring in magic method "D200", # One-line docstring should fit on one line with quotes "D400", # First line should end with a period + "D401", # First line of docstring should be in imperative mood ] # Provide some leeway for long docstring, this is otherwise handled by black line-length = 110 diff --git a/src/fgen_example/_lib/derived_type.yaml b/src/fgen_example/_lib/derived_type.yaml index 565df22..584451f 100644 --- a/src/fgen_example/_lib/derived_type.yaml +++ b/src/fgen_example/_lib/derived_type.yaml @@ -5,27 +5,33 @@ provides: description: An example of a derived type attributes: base: - description: Base value + definition: + description: Base value + fortran_type: real(8) unit: m - fortran_type: real(8) methods: add: description: Add another value to `self.base` parameters: other: - description: Quantity to add + definition: + description: Quantity to add + fortran_type: real(8) unit: m - fortran_type: real(8) + returns: - name: output - description: Sum of `self.base` and `other` + definition: + name: output + description: Sum of `self.base` and `other` + fortran_type: real(8) unit: m - fortran_type: real(8) + double: description: Double `self.base` parameters: {} returns: - name: output - description: Double `self.base` + definition: + name: output + description: Double `self.base` + fortran_type: real(8) unit: m - fortran_type: real(8) diff --git a/src/fgen_example/_lib/derived_type_manager.f90 b/src/fgen_example/_lib/derived_type_manager.f90 index f57fa09..a8feebe 100644 --- a/src/fgen_example/_lib/derived_type_manager.f90 +++ b/src/fgen_example/_lib/derived_type_manager.f90 @@ -1,10 +1,13 @@ ! -! Manager for the lifecycle of the DerivedType calculator +! Manager for ``derived_type``'s ``DerivedType`` derived type ! +! In combination with ``derived_type_w``, +! this allows the ``DerivedType`` derived type +! to be exposed to Python. +! +module derived_type_manager -module mod_derived_type_manager - use derived_type, only: & - DerivedType + use derived_type, only: DerivedType use fgen_utils, only: & finalize_derived_type_instance_number, & get_derived_type_free_instance_number @@ -12,73 +15,90 @@ module mod_derived_type_manager implicit none private - integer, parameter :: N_MODELS = 2048 + integer, parameter :: N_INSTANCES = 4096 - type(DerivedType), target, dimension(N_MODELS) :: instance_array - logical, dimension(N_MODELS) :: instance_available = .true. + type(DerivedType), target, dimension(N_INSTANCES) :: instance_array + logical, dimension(N_INSTANCES) :: instance_available = .true. public :: get_free_instance_number, & get_instance, & - instance_finalize, & - check_index_claimed + instance_finalize contains - function get_free_instance_number() result(model_index) + function get_free_instance_number() result(instance_index) + ! Get the index of a free instance - integer :: model_index + integer :: instance_index + ! Free instance index call get_derived_type_free_instance_number( & - model_index, & - N_MODELS, & + instance_index, & + N_INSTANCES, & instance_available, & instance_array & ) end function get_free_instance_number - subroutine get_instance(model_index, instance) - ! Get an instance for a given model index + subroutine get_instance(instance_index, instance_pointer) + ! Associate a pointer with the instance corresponding to the given model index ! - ! This will stop execution if the instance has not already been initialised + ! Stops execution if the instance has not already been initialised. + + integer, intent(in) :: instance_index + ! Index of the instance to point to - integer, intent(in) :: model_index - type(DerivedType), pointer, intent(inout) :: instance + type(DerivedType), pointer, intent(inout) :: instance_pointer + ! Pointer to associate - call check_index_claimed(model_index) - instance => instance_array(model_index) + call check_index_claimed(instance_index) + instance_pointer => instance_array(instance_index) end subroutine get_instance - subroutine instance_finalize(model_index) + subroutine instance_finalize(instance_index) + ! Finalise an instance - integer, intent(in) :: model_index + integer, intent(in) :: instance_index + ! Index of the instance to finalise - call check_index_claimed(model_index) + call check_index_claimed(instance_index) call finalize_derived_type_instance_number( & - model_index, & - N_MODELS, & + instance_index, & + N_INSTANCES, & instance_available, & instance_array & ) end subroutine instance_finalize - ! - ! Private routines - ! + subroutine check_index_claimed(instance_index) + ! Check that an index has already been claimed + ! + ! Stops execution if the index has not been claimed. - subroutine check_index_claimed(model_index) + integer, intent(in) :: instance_index + ! Instance index to check - integer, intent(in) :: model_index + if (instance_available(instance_index)) then + print *, "Index ", instance_index, " has not been claimed" + error stop 1 + end if + + if (instance_index < 1) then + ! TODO: return error code to python + print *, "Requested index is ", instance_index, " which is less than 1" + error stop 1 + end if - if (instance_available(model_index) & - .or. model_index < 1 & - .or. instance_array(model_index)%model_index < 1) then + if (instance_array(instance_index)%instance_index < 1) then ! TODO: return error code to python - stop + print *, "Index ", instance_index, " is associated with an instance that has instance index < 1", & + "instance's instance_index attribute ", instance_array(instance_index)%instance_index + error stop 1 end if end subroutine check_index_claimed -end module mod_derived_type_manager +end module derived_type_manager diff --git a/src/fgen_example/_lib/derived_type_wrapped.f90 b/src/fgen_example/_lib/derived_type_wrapped.f90 index 90ce669..376b919 100644 --- a/src/fgen_example/_lib/derived_type_wrapped.f90 +++ b/src/fgen_example/_lib/derived_type_wrapped.f90 @@ -1,127 +1,172 @@ +!!! +! Wrapper for ``derived_type`` ! -! Wrapper for mod_derived_type_manager -! Exposes the DerivedType calculator -! -module w_derived_type +! In combination with ``derived_type_manager``, +! this allows the ``DerivedType`` derived type +! to be exposed to Python. +!!! +module derived_type_w + + ! Standard library requirements use iso_c_binding, only: c_loc, c_ptr - use mod_derived_type_manager, only: & + + ! First-party requirements from the module we're wrapping + use derived_type, only: DerivedType + use derived_type_manager, only: & manager_get_free_instance => get_free_instance_number, & manager_instance_finalize => instance_finalize, & - manager_get_instance => get_instance, & - check_index_claimed - - use derived_type, only: & - DerivedType + manager_get_instance => get_instance implicit none private - ! TODO: handle cases where more complicated wrappers are needed - public :: get_free_instance, & + public :: get_free_instance_number, & instance_build, & instance_finalize - ! Getters - public :: ig_base - ! Calculator methods + ! Statment declarations for getters and setters + public :: iget_base + public :: iset_base + + ! Statement declarations for methods public :: i_add public :: i_double -contains - function get_free_instance() result(model_index) - - integer :: model_index - - model_index = manager_get_free_instance() - - end function get_free_instance +contains - subroutine instance_finalize(model_index) + function get_free_instance_number() result(instance_index) - integer, intent(in) :: model_index + integer :: instance_index - call manager_instance_finalize(model_index) + instance_index = manager_get_free_instance() - end subroutine instance_finalize + end function get_free_instance_number + ! Build methods ! - ! Build a new instance - ! + ! These are a bit like Python class methods, + ! but they build/intialise/set up the class + ! rather than returning a new instance. subroutine instance_build( & - model_index, & + instance_index, & base & ) - integer, intent(in) :: model_index + integer, intent(in) :: instance_index + real(8), intent(in) :: base + ! Passing of base + type(DerivedType), pointer :: instance - call manager_get_instance(model_index, instance) + call manager_get_instance(instance_index, instance) call instance%build( & - base & + base=base & ) end subroutine instance_build - ! - ! Calculator accessors - ! + ! Finalisation + subroutine instance_finalize(instance_index) + + integer, intent(in) :: instance_index + + call manager_instance_finalize(instance_index) + + end subroutine instance_finalize + + ! Attributes getters and setters + ! Wrapping base + ! Strategy: WrappingStrategyDefault( + ! magnitude_suffix='_m', + ! ) + subroutine iget_base( & + instance_index, & + base & + ) - function ig_base(model_index) result(base) + integer, intent(in) :: instance_index - integer, intent(in) :: model_index - real(8) :: base + real(8), intent(out) :: base + ! Returning of base type(DerivedType), pointer :: instance - call manager_get_instance(model_index, instance) + call manager_get_instance(instance_index, instance) base = instance%base - end function ig_base + end subroutine iget_base - ! - ! Calculator methods - ! + subroutine iset_base( & + instance_index, & + base & + ) - function i_add( & - model_index, & - other & - ) result(output) + integer, intent(in) :: instance_index - ! Should work out consistent approach to whether we use intent or not... - integer :: model_index - real(8) :: other - real(8) :: output + real(8), intent(in) :: base + ! Passing of base type(DerivedType), pointer :: instance - call manager_get_instance(model_index, instance) + call manager_get_instance(instance_index, instance) + + instance%base = base + + end subroutine iset_base + + ! Wrapped methods + ! Wrapping output + ! Strategy: WrappingStrategyDefault( + ! magnitude_suffix='_m', + ! ) + subroutine i_add( & + instance_index, & + other, & + output & + ) - !&< - output = instance % add( & - other & - ) - !&> - end function i_add + integer, intent(in) :: instance_index - function i_double( & - model_index & - ) result(output) + real(8), intent(in) :: other + ! Passing of other - ! Should work out consistent approach to whether we use intent or not... - integer :: model_index - real(8) :: output + real(8), intent(out) :: output + ! Returning of output type(DerivedType), pointer :: instance - call manager_get_instance(model_index, instance) + call manager_get_instance(instance_index, instance) + + output = instance%add( & + other=other & + ) + + end subroutine i_add + + ! Wrapping output + ! Strategy: WrappingStrategyDefault( + ! magnitude_suffix='_m', + ! ) + subroutine i_double( & + instance_index, & + output & + ) + + integer, intent(in) :: instance_index + + real(8), intent(out) :: output + ! Returning of output + + type(DerivedType), pointer :: instance + + call manager_get_instance(instance_index, instance) + + output = instance%double( & + ) - !&< - output = instance % double( & - ) - !&> - end function i_double + end subroutine i_double -end module w_derived_type +end module derived_type_w diff --git a/src/fgen_example/_lib/operations.yaml b/src/fgen_example/_lib/operations.yaml index 57079e3..697f110 100644 --- a/src/fgen_example/_lib/operations.yaml +++ b/src/fgen_example/_lib/operations.yaml @@ -8,9 +8,10 @@ provides: # (see https://gitlab.com/magicc/fgen/-/issues/14) attributes: weight: - description: Weight to apply to operations + definition: + description: Weight to apply to operations + fortran_type: real(8) unit: dimensionless - fortran_type: real(8) methods: calc_vec_prod_sum: description: Calculate vector product then sum then multiply by `self % weight` @@ -18,15 +19,18 @@ provides: # (see https://gitlab.com/magicc/fgen/-/issues/14) parameters: a: - description: first vector + definition: + description: first vector + fortran_type: real(8), dimension(3) unit: dimensionless - fortran_type: real(8), dimension(3) b: - description: second vector + definition: + description: second vector + fortran_type: real(8), dimension(3) unit: dimensionless - fortran_type: real(8), dimension(3) returns: - name: vec_prod_sum - description: Result of doing vector product then sum then multiplying by `self % weight` + definition: + name: vec_prod_sum + description: Result of doing vector product then sum then multiplying by `self % weight` + fortran_type: real(8) unit: dimensionless - fortran_type: real(8) diff --git a/src/fgen_example/_lib/operations_manager.f90 b/src/fgen_example/_lib/operations_manager.f90 index 4ec1edd..d568a98 100644 --- a/src/fgen_example/_lib/operations_manager.f90 +++ b/src/fgen_example/_lib/operations_manager.f90 @@ -1,10 +1,13 @@ ! -! Manager for the lifecycle of the Operator calculator +! Manager for ``operations``'s ``Operator`` derived type ! +! In combination with ``operations_w``, +! this allows the ``Operator`` derived type +! to be exposed to Python. +! +module operations_manager -module mod_operations_manager - use operations, only: & - Operator + use operations, only: Operator use fgen_utils, only: & finalize_derived_type_instance_number, & get_derived_type_free_instance_number @@ -12,73 +15,90 @@ module mod_operations_manager implicit none private - integer, parameter :: N_MODELS = 2048 + integer, parameter :: N_INSTANCES = 4096 - type(Operator), target, dimension(N_MODELS) :: instance_array - logical, dimension(N_MODELS) :: instance_available = .true. + type(Operator), target, dimension(N_INSTANCES) :: instance_array + logical, dimension(N_INSTANCES) :: instance_available = .true. public :: get_free_instance_number, & get_instance, & - instance_finalize, & - check_index_claimed + instance_finalize contains - function get_free_instance_number() result(model_index) + function get_free_instance_number() result(instance_index) + ! Get the index of a free instance - integer :: model_index + integer :: instance_index + ! Free instance index call get_derived_type_free_instance_number( & - model_index, & - N_MODELS, & + instance_index, & + N_INSTANCES, & instance_available, & instance_array & ) end function get_free_instance_number - subroutine get_instance(model_index, instance) - ! Get an instance for a given model index + subroutine get_instance(instance_index, instance_pointer) + ! Associate a pointer with the instance corresponding to the given model index ! - ! This will stop execution if the instance has not already been initialised + ! Stops execution if the instance has not already been initialised. + + integer, intent(in) :: instance_index + ! Index of the instance to point to - integer, intent(in) :: model_index - type(Operator), pointer, intent(inout) :: instance + type(Operator), pointer, intent(inout) :: instance_pointer + ! Pointer to associate - call check_index_claimed(model_index) - instance => instance_array(model_index) + call check_index_claimed(instance_index) + instance_pointer => instance_array(instance_index) end subroutine get_instance - subroutine instance_finalize(model_index) + subroutine instance_finalize(instance_index) + ! Finalise an instance - integer, intent(in) :: model_index + integer, intent(in) :: instance_index + ! Index of the instance to finalise - call check_index_claimed(model_index) + call check_index_claimed(instance_index) call finalize_derived_type_instance_number( & - model_index, & - N_MODELS, & + instance_index, & + N_INSTANCES, & instance_available, & instance_array & ) end subroutine instance_finalize - ! - ! Private routines - ! + subroutine check_index_claimed(instance_index) + ! Check that an index has already been claimed + ! + ! Stops execution if the index has not been claimed. - subroutine check_index_claimed(model_index) + integer, intent(in) :: instance_index + ! Instance index to check - integer, intent(in) :: model_index + if (instance_available(instance_index)) then + print *, "Index ", instance_index, " has not been claimed" + error stop 1 + end if + + if (instance_index < 1) then + ! TODO: return error code to python + print *, "Requested index is ", instance_index, " which is less than 1" + error stop 1 + end if - if (instance_available(model_index) & - .or. model_index < 1 & - .or. instance_array(model_index)%model_index < 1) then + if (instance_array(instance_index)%instance_index < 1) then ! TODO: return error code to python - stop + print *, "Index ", instance_index, " is associated with an instance that has instance index < 1", & + "instance's instance_index attribute ", instance_array(instance_index)%instance_index + error stop 1 end if end subroutine check_index_claimed -end module mod_operations_manager +end module operations_manager diff --git a/src/fgen_example/_lib/operations_wrapped.f90 b/src/fgen_example/_lib/operations_wrapped.f90 index f28d80e..fa8406c 100644 --- a/src/fgen_example/_lib/operations_wrapped.f90 +++ b/src/fgen_example/_lib/operations_wrapped.f90 @@ -1,111 +1,153 @@ +!!! +! Wrapper for ``operations`` ! -! Wrapper for mod_operations_manager -! Exposes the Operator calculator -! -module w_operations +! In combination with ``operations_manager``, +! this allows the ``Operator`` derived type +! to be exposed to Python. +!!! +module operations_w + + ! Standard library requirements use iso_c_binding, only: c_loc, c_ptr - use mod_operations_manager, only: & + + ! First-party requirements from the module we're wrapping + use operations, only: Operator + use operations_manager, only: & manager_get_free_instance => get_free_instance_number, & manager_instance_finalize => instance_finalize, & - manager_get_instance => get_instance, & - check_index_claimed - - use operations, only: & - Operator + manager_get_instance => get_instance implicit none private - ! TODO: handle cases where more complicated wrappers are needed - public :: get_free_instance, & + public :: get_free_instance_number, & instance_build, & instance_finalize - ! Getters - public :: ig_weight - ! Calculator methods - public :: i_calc_vec_prod_sum -contains - - function get_free_instance() result(model_index) - - integer :: model_index + ! Statment declarations for getters and setters + public :: iget_weight + public :: iset_weight - model_index = manager_get_free_instance() + ! Statement declarations for methods + public :: i_calc_vec_prod_sum - end function get_free_instance +contains - subroutine instance_finalize(model_index) + function get_free_instance_number() result(instance_index) - integer, intent(in) :: model_index + integer :: instance_index - call manager_instance_finalize(model_index) + instance_index = manager_get_free_instance() - end subroutine instance_finalize + end function get_free_instance_number + ! Build methods ! - ! Build a new instance - ! + ! These are a bit like Python class methods, + ! but they build/intialise/set up the class + ! rather than returning a new instance. subroutine instance_build( & - model_index, & + instance_index, & weight & ) - integer, intent(in) :: model_index + integer, intent(in) :: instance_index + real(8), intent(in) :: weight + ! Passing of weight + type(Operator), pointer :: instance - call manager_get_instance(model_index, instance) + call manager_get_instance(instance_index, instance) call instance%build( & - weight & + weight=weight & ) end subroutine instance_build - ! - ! Calculator accessors - ! + ! Finalisation + subroutine instance_finalize(instance_index) + + integer, intent(in) :: instance_index + + call manager_instance_finalize(instance_index) + + end subroutine instance_finalize - function ig_weight(model_index) result(weight) + ! Attributes getters and setters + ! Wrapping weight + ! Strategy: WrappingStrategyDefault( + ! magnitude_suffix='_m', + ! ) + subroutine iget_weight( & + instance_index, & + weight & + ) + + integer, intent(in) :: instance_index - integer, intent(in) :: model_index - real(8) :: weight + real(8), intent(out) :: weight + ! Returning of weight type(Operator), pointer :: instance - call manager_get_instance(model_index, instance) + call manager_get_instance(instance_index, instance) weight = instance%weight - end function ig_weight + end subroutine iget_weight - ! - ! Calculator methods - ! + subroutine iset_weight( & + instance_index, & + weight & + ) + + integer, intent(in) :: instance_index + + real(8), intent(in) :: weight + ! Passing of weight - function i_calc_vec_prod_sum( & - model_index, & + type(Operator), pointer :: instance + + call manager_get_instance(instance_index, instance) + + instance%weight = weight + + end subroutine iset_weight + + ! Wrapped methods + ! Wrapping vec_prod_sum + ! Strategy: WrappingStrategyDefault( + ! magnitude_suffix='_m', + ! ) + subroutine i_calc_vec_prod_sum( & + instance_index, & a, & - b & - ) result(vec_prod_sum) + b, & + vec_prod_sum & + ) + + integer, intent(in) :: instance_index + + real(8), dimension(3), intent(in) :: a + ! Passing of a - ! Should work out consistent approach to whether we use intent or not... - integer :: model_index - real(8), dimension(3) :: a - real(8), dimension(3) :: b - real(8) :: vec_prod_sum + real(8), dimension(3), intent(in) :: b + ! Passing of b + + real(8), intent(out) :: vec_prod_sum + ! Returning of vec_prod_sum type(Operator), pointer :: instance - call manager_get_instance(model_index, instance) + call manager_get_instance(instance_index, instance) + + vec_prod_sum = instance%calc_vec_prod_sum( & + a=a, & + b=b & + ) - !&< - vec_prod_sum = instance % calc_vec_prod_sum( & - a, & - b & - ) - !&> - end function i_calc_vec_prod_sum + end subroutine i_calc_vec_prod_sum -end module w_operations +end module operations_w diff --git a/src/fgen_example/derived_type.py b/src/fgen_example/derived_type.py index 9761efa..40da6b3 100644 --- a/src/fgen_example/derived_type.py +++ b/src/fgen_example/derived_type.py @@ -1,5 +1,8 @@ """ -Python wrapper of Fortran module derived_type +Python wrapper of the Fortran module ``derived_type_w`` + +``derived_type_w`` is itself a wrapper +around the Fortran module ``derived_type``. """ from __future__ import annotations @@ -8,17 +11,21 @@ import fgen_runtime.exceptions as fgr_excs from attrs import define from fgen_runtime.base import ( - INVALID_MODEL_INDEX, + INVALID_INSTANCE_INDEX, FinalizableWrapperBase, FinalizableWrapperBaseContext, check_initialised, execute_finalize_on_fail, ) -from fgen_runtime.exceptions import PointerArrayConversionError +from fgen_runtime.formatting import ( + to_html, + to_pretty, + to_str, +) from fgen_runtime.units import verify_units try: - from fgen_example._lib import w_derived_type # type: ignore + from fgen_example._lib import derived_type_w # type: ignore except (ModuleNotFoundError, ImportError) as exc: raise fgr_excs.CompiledExtensionNotFoundError("fgen_example._lib") from exc @@ -37,59 +44,276 @@ class DerivedType(FinalizableWrapperBase): An example of a derived type """ + @property + def exposed_attributes(self) -> tuple[str, ...]: + """ + Attributes exposed by this wrapper + """ + return ("base",) + def __str__(self) -> str: - if self.model_index == INVALID_MODEL_INDEX: - return f"Uninitialised {self!r}" - - props = [ - "base", - ] - prop_vals = [] - for p in props: - try: - prop_vals.append(f"{p}={getattr(self, p)}") - except PointerArrayConversionError: - prop_vals.append( - f"{p} could not be retrieved from its pointer, perhaps it is unset?", - ) - - base = repr(self) - out = f"{base[:-1]}, {', '.join(prop_vals)})" + """ + String representation of self + """ + return to_str( + self, + self.exposed_attributes, + ) + + def _repr_pretty_(self, p: Any, cycle: bool) -> None: + """ + Pretty representation of self + + Used by IPython notebooks and other tools + """ + to_pretty( + self, + self.exposed_attributes, + p=p, + cycle=cycle, + ) + + def _repr_html_(self) -> str: + """ + html representation of self + + Used by IPython notebooks and other tools + """ + return to_html( + self, + self.exposed_attributes, + ) + + # Class methods + @classmethod + @verify_units( + None, + ( + None, + _UNITS["base"], + ), + ) + def from_build_args( + cls, + base: float, + ) -> DerivedType: + """ + Initialise from build arguments + + This also creates a new connection to a Fortran object. + The user is responsible for releasing this connection + using :attr:`~finalize` when it is no longer needed. + Alternatively a :obj:`~DerivedTypeContext` + can be used to handle the finalisation using a context manager. + + Parameters + ---------- + base + Base value + + Returns + ------- + Built (i.e. linked to Fortran and initialised) + :obj:`DerivedType` + + See Also + -------- + :meth:`DerivedTypeContext.from_build_args` + """ + out = cls.from_new_connection() + execute_finalize_on_fail( + out, + derived_type_w.instance_build, + base=base, + ) return out @classmethod def from_new_connection(cls) -> DerivedType: """ - Allocate a new calculator instance + Initialise from a new connection + + The user is responsible for releasing this connection + using :attr:`~finalize` when it is no longer needed. + Alternatively a :obj:`~DerivedTypeContext` + can be used to handle the finalisation using a context manager. Returns ------- - A new instance with a unique model index + A new instance with a unique instance index Raises ------ WrapperErrorUnknownCause If a new instance could not be allocated - This could occur if too many models are allocated at any one time + This could occur if too many instances are allocated at any one time """ - model_index = w_derived_type.get_free_instance() - if model_index == INVALID_MODEL_INDEX: + instance_index = derived_type_w.get_free_instance_number() + if instance_index == INVALID_INSTANCE_INDEX: raise fgr_excs.WrapperErrorUnknownCause( # noqa: TRY003 f"Could not create instance of {cls.__name__}. " ) - return cls(model_index) + return cls(instance_index) + # Finalisation @check_initialised def finalize(self) -> None: """ Close the connection with the Fortran module """ - w_derived_type.instance_finalize(self.model_index) - self._uninitialise_model_index() + derived_type_w.instance_finalize(self.instance_index) + self._uninitialise_instance_index() + + # Attribute getters and setters + @property + @check_initialised + @verify_units( + _UNITS["base"], + (None,), + ) + def base(self) -> float: + """ + Base value + + Returns + ------- + Attribute value, retrieved from Fortran. + + The value is a copy of the derived type's data. + Changes to this value will not be reflected + in the underlying instance of the derived type. + To make changes to the underlying instance, use the setter instead. + """ + # Wrapping base + # Strategy: WrappingStrategyDefault( + # magnitude_suffix='_m', + # ) + base: float = derived_type_w.iget_base( + self.instance_index, + ) + + return base + + # Wrapped methods + @check_initialised + @verify_units( + _UNITS["output"], + ( + None, + _UNITS["other"], + ), + ) + def add( + self, + other: float, + ) -> float: + """ + Add another value to `self.base` + + Parameters + ---------- + other + Quantity to add + + Returns + ------- + Sum of `self.base` and `other` + """ + # Wrapping output + # Strategy: WrappingStrategyDefault( + # magnitude_suffix='_m', + # ) + output: float = derived_type_w.i_add( + self.instance_index, + other=other, + ) + + return output + + @check_initialised + @verify_units( + _UNITS["output"], + (None,), + ) + def double( + self, + ) -> float: + """ + Double `self.base` + + Returns + ------- + Double `self.base` + """ + # Wrapping output + # Strategy: WrappingStrategyDefault( + # magnitude_suffix='_m', + # ) + output: float = derived_type_w.i_double( + self.instance_index, + ) + + return output + + +@define +class DerivedTypeNoSetters(FinalizableWrapperBase): + """ + Wrapper around the Fortran :class:`DerivedType` + This wrapper has no setters so can be used for representing objects + that have no connection to the underlying Fortran + (i.e. changing their values/attributes + will have no effect on the underlying Fortran). + For example, derived type attribute values that are allocatable. + + An example of a derived type + """ + + @property + def exposed_attributes(self) -> tuple[str, ...]: + """ + Attributes exposed by this wrapper + """ + return ("base",) + + def __str__(self) -> str: + """ + String representation of self + """ + return to_str( + self, + self.exposed_attributes, + ) + + def _repr_pretty_(self, p: Any, cycle: bool) -> None: + """ + Pretty representation of self + + Used by IPython notebooks and other tools + """ + to_pretty( + self, + self.exposed_attributes, + p=p, + cycle=cycle, + ) + + def _repr_html_(self) -> str: + """ + html representation of self + + Used by IPython notebooks and other tools + """ + return to_html( + self, + self.exposed_attributes, + ) + + # Class methods @classmethod @verify_units( None, @@ -101,29 +325,108 @@ def finalize(self) -> None: def from_build_args( cls, base: float, - ) -> DerivedType: + ) -> DerivedTypeNoSetters: """ - Build a new DerivedType + Initialise from build arguments + + This also creates a new connection to a Fortran object. + The user is responsible for releasing this connection + using :attr:`~finalize` when it is no longer needed. + Alternatively a :obj:`~DerivedTypeNoSettersContext` + can be used to handle the finalisation using a context manager. - Creates a new connection to a Fortran object. The user is responsible for releasing this connection - using :attr:`~finalize` when it is no longer needed. Alternatively a - :class:`DerivedTypeContext` - can be used to handle the finalization using a context manager. + Parameters + ---------- + base + Base value + + Returns + ------- + Built (i.e. linked to Fortran and initialised) + :obj:`DerivedTypeNoSetters` See Also -------- - :meth:`DerivedTypeContext.from_build_args` + :meth:`DerivedTypeNoSettersContext.from_build_args` """ out = cls.from_new_connection() - execute_finalize_on_fail( out, - w_derived_type.instance_build, + derived_type_w.instance_build, base=base, ) return out + @classmethod + def from_new_connection(cls) -> DerivedTypeNoSetters: + """ + Initialise from a new connection + + The user is responsible for releasing this connection + using :attr:`~finalize` when it is no longer needed. + Alternatively a :obj:`~DerivedTypeNoSettersContext` + can be used to handle the finalisation using a context manager. + + Returns + ------- + A new instance with a unique instance index + + Raises + ------ + WrapperErrorUnknownCause + If a new instance could not be allocated + + This could occur if too many instances are allocated at any one time + """ + instance_index = derived_type_w.get_free_instance_number() + if instance_index == INVALID_INSTANCE_INDEX: + raise fgr_excs.WrapperErrorUnknownCause( # noqa: TRY003 + f"Could not create instance of {cls.__name__}. " + ) + + return cls(instance_index) + + # Finalisation + @check_initialised + def finalize(self) -> None: + """ + Close the connection with the Fortran module + """ + derived_type_w.instance_finalize(self.instance_index) + self._uninitialise_instance_index() + + # Attribute getters + @property + @check_initialised + @verify_units( + _UNITS["base"], + (None,), + ) + def base(self) -> float: + """ + Base value + + Returns + ------- + Attribute value, retrieved from Fortran. + + The value is a copy of the derived type's data. + Changes to this value will not be reflected + in the underlying instance of the derived type. + To make changes to the underlying instance, use the setter instead. + """ + # Wrapping base + # Strategy: WrappingStrategyDefault( + # magnitude_suffix='_m', + # ) + base: float = derived_type_w.iget_base( + self.instance_index, + ) + + return base + + # Wrapped methods @check_initialised @verify_units( _UNITS["output"], @@ -138,13 +441,26 @@ def add( ) -> float: """ Add another value to `self.base` + + Parameters + ---------- + other + Quantity to add + + Returns + ------- + Sum of `self.base` and `other` """ - out: float = w_derived_type.i_add( - self.model_index, - other, + # Wrapping output + # Strategy: WrappingStrategyDefault( + # magnitude_suffix='_m', + # ) + output: float = derived_type_w.i_add( + self.instance_index, + other=other, ) - return out + return output @check_initialised @verify_units( @@ -156,29 +472,20 @@ def double( ) -> float: """ Double `self.base` - """ - out: float = w_derived_type.i_double( - self.model_index, - ) - - return out - @property - @check_initialised - @verify_units( - _UNITS["base"], - (None,), - ) - def base(self) -> float: - """ - Base value + Returns + ------- + Double `self.base` """ - # fmt: off - out: float \ - = w_derived_type.ig_base(self.model_index) - # fmt: on + # Wrapping output + # Strategy: WrappingStrategyDefault( + # magnitude_suffix='_m', + # ) + output: float = derived_type_w.i_double( + self.instance_index, + ) - return out + return output @define @@ -199,3 +506,23 @@ def from_build_args( return cls( DerivedType.from_build_args(*args, **kwargs), ) + + +@define +class DerivedTypeNoSettersContext(FinalizableWrapperBaseContext): + """ + Context manager for :class:`DerivedTypeNoSetters` + """ + + @classmethod + def from_build_args( + cls, + *args: Any, + **kwargs: Any, + ) -> DerivedTypeNoSettersContext: + """ + Docstrings to be handled as part of #223 + """ + return cls( + DerivedTypeNoSetters.from_build_args(*args, **kwargs), + ) diff --git a/src/fgen_example/operations.py b/src/fgen_example/operations.py index 38c4233..a5540f0 100644 --- a/src/fgen_example/operations.py +++ b/src/fgen_example/operations.py @@ -1,5 +1,8 @@ """ -Python wrapper of Fortran module operations +Python wrapper of the Fortran module ``operations_w`` + +``operations_w`` is itself a wrapper +around the Fortran module ``operations``. """ from __future__ import annotations @@ -8,17 +11,21 @@ import fgen_runtime.exceptions as fgr_excs from attrs import define from fgen_runtime.base import ( - INVALID_MODEL_INDEX, + INVALID_INSTANCE_INDEX, FinalizableWrapperBase, FinalizableWrapperBaseContext, check_initialised, execute_finalize_on_fail, ) -from fgen_runtime.exceptions import PointerArrayConversionError +from fgen_runtime.formatting import ( + to_html, + to_pretty, + to_str, +) from fgen_runtime.units import verify_units try: - from fgen_example._lib import w_operations # type: ignore + from fgen_example._lib import operations_w # type: ignore except (ModuleNotFoundError, ImportError) as exc: raise fgr_excs.CompiledExtensionNotFoundError("fgen_example._lib") from exc @@ -38,59 +45,257 @@ class Operator(FinalizableWrapperBase): An example of another derived type """ + @property + def exposed_attributes(self) -> tuple[str, ...]: + """ + Attributes exposed by this wrapper + """ + return ("weight",) + def __str__(self) -> str: - if self.model_index == INVALID_MODEL_INDEX: - return f"Uninitialised {self!r}" - - props = [ - "weight", - ] - prop_vals = [] - for p in props: - try: - prop_vals.append(f"{p}={getattr(self, p)}") - except PointerArrayConversionError: - prop_vals.append( - f"{p} could not be retrieved from its pointer, perhaps it is unset?", - ) - - base = repr(self) - out = f"{base[:-1]}, {', '.join(prop_vals)})" + """ + String representation of self + """ + return to_str( + self, + self.exposed_attributes, + ) + + def _repr_pretty_(self, p: Any, cycle: bool) -> None: + """ + Pretty representation of self + + Used by IPython notebooks and other tools + """ + to_pretty( + self, + self.exposed_attributes, + p=p, + cycle=cycle, + ) + + def _repr_html_(self) -> str: + """ + html representation of self + + Used by IPython notebooks and other tools + """ + return to_html( + self, + self.exposed_attributes, + ) + + # Class methods + @classmethod + @verify_units( + None, + ( + None, + _UNITS["weight"], + ), + ) + def from_build_args( + cls, + weight: float, + ) -> Operator: + """ + Initialise from build arguments + + This also creates a new connection to a Fortran object. + The user is responsible for releasing this connection + using :attr:`~finalize` when it is no longer needed. + Alternatively a :obj:`~OperatorContext` + can be used to handle the finalisation using a context manager. + + Parameters + ---------- + weight + Weight to apply to operations + + Returns + ------- + Built (i.e. linked to Fortran and initialised) + :obj:`Operator` + + See Also + -------- + :meth:`OperatorContext.from_build_args` + """ + out = cls.from_new_connection() + execute_finalize_on_fail( + out, + operations_w.instance_build, + weight=weight, + ) return out @classmethod def from_new_connection(cls) -> Operator: """ - Allocate a new calculator instance + Initialise from a new connection + + The user is responsible for releasing this connection + using :attr:`~finalize` when it is no longer needed. + Alternatively a :obj:`~OperatorContext` + can be used to handle the finalisation using a context manager. Returns ------- - A new instance with a unique model index + A new instance with a unique instance index Raises ------ WrapperErrorUnknownCause If a new instance could not be allocated - This could occur if too many models are allocated at any one time + This could occur if too many instances are allocated at any one time """ - model_index = w_operations.get_free_instance() - if model_index == INVALID_MODEL_INDEX: + instance_index = operations_w.get_free_instance_number() + if instance_index == INVALID_INSTANCE_INDEX: raise fgr_excs.WrapperErrorUnknownCause( # noqa: TRY003 f"Could not create instance of {cls.__name__}. " ) - return cls(model_index) + return cls(instance_index) + # Finalisation @check_initialised def finalize(self) -> None: """ Close the connection with the Fortran module """ - w_operations.instance_finalize(self.model_index) - self._uninitialise_model_index() + operations_w.instance_finalize(self.instance_index) + self._uninitialise_instance_index() + + # Attribute getters and setters + @property + @check_initialised + @verify_units( + _UNITS["weight"], + (None,), + ) + def weight(self) -> float: + """ + Weight to apply to operations + + Returns + ------- + Attribute value, retrieved from Fortran. + + The value is a copy of the derived type's data. + Changes to this value will not be reflected + in the underlying instance of the derived type. + To make changes to the underlying instance, use the setter instead. + """ + # Wrapping weight + # Strategy: WrappingStrategyDefault( + # magnitude_suffix='_m', + # ) + weight: float = operations_w.iget_weight( + self.instance_index, + ) + + return weight + + # Wrapped methods + @check_initialised + @verify_units( + _UNITS["vec_prod_sum"], + ( + None, + _UNITS["a"], + _UNITS["b"], + ), + ) + def calc_vec_prod_sum( + self, + a: tuple[float, float, float], + b: tuple[float, float, float], + ) -> float: + """ + Calculate vector product then sum then multiply by `self % weight` + + Parameters + ---------- + a + first vector + + b + second vector + + Returns + ------- + Result of doing vector product then sum then multiplying by `self % weight` + """ + # Wrapping vec_prod_sum + # Strategy: WrappingStrategyDefault( + # magnitude_suffix='_m', + # ) + vec_prod_sum: float = operations_w.i_calc_vec_prod_sum( + self.instance_index, + a=a, + b=b, + ) + + return vec_prod_sum + + +@define +class OperatorNoSetters(FinalizableWrapperBase): + """ + Wrapper around the Fortran :class:`Operator` + + This wrapper has no setters so can be used for representing objects + that have no connection to the underlying Fortran + (i.e. changing their values/attributes + will have no effect on the underlying Fortran). + For example, derived type attribute values that are allocatable. + + An example of another derived type + """ + + @property + def exposed_attributes(self) -> tuple[str, ...]: + """ + Attributes exposed by this wrapper + """ + return ("weight",) + def __str__(self) -> str: + """ + String representation of self + """ + return to_str( + self, + self.exposed_attributes, + ) + + def _repr_pretty_(self, p: Any, cycle: bool) -> None: + """ + Pretty representation of self + + Used by IPython notebooks and other tools + """ + to_pretty( + self, + self.exposed_attributes, + p=p, + cycle=cycle, + ) + + def _repr_html_(self) -> str: + """ + html representation of self + + Used by IPython notebooks and other tools + """ + return to_html( + self, + self.exposed_attributes, + ) + + # Class methods @classmethod @verify_units( None, @@ -102,29 +307,108 @@ def finalize(self) -> None: def from_build_args( cls, weight: float, - ) -> Operator: + ) -> OperatorNoSetters: """ - Build a new Operator + Initialise from build arguments + + This also creates a new connection to a Fortran object. + The user is responsible for releasing this connection + using :attr:`~finalize` when it is no longer needed. + Alternatively a :obj:`~OperatorNoSettersContext` + can be used to handle the finalisation using a context manager. + + Parameters + ---------- + weight + Weight to apply to operations - Creates a new connection to a Fortran object. The user is responsible for releasing this connection - using :attr:`~finalize` when it is no longer needed. Alternatively a - :class:`OperatorContext` - can be used to handle the finalization using a context manager. + Returns + ------- + Built (i.e. linked to Fortran and initialised) + :obj:`OperatorNoSetters` See Also -------- - :meth:`OperatorContext.from_build_args` + :meth:`OperatorNoSettersContext.from_build_args` """ out = cls.from_new_connection() - execute_finalize_on_fail( out, - w_operations.instance_build, + operations_w.instance_build, weight=weight, ) return out + @classmethod + def from_new_connection(cls) -> OperatorNoSetters: + """ + Initialise from a new connection + + The user is responsible for releasing this connection + using :attr:`~finalize` when it is no longer needed. + Alternatively a :obj:`~OperatorNoSettersContext` + can be used to handle the finalisation using a context manager. + + Returns + ------- + A new instance with a unique instance index + + Raises + ------ + WrapperErrorUnknownCause + If a new instance could not be allocated + + This could occur if too many instances are allocated at any one time + """ + instance_index = operations_w.get_free_instance_number() + if instance_index == INVALID_INSTANCE_INDEX: + raise fgr_excs.WrapperErrorUnknownCause( # noqa: TRY003 + f"Could not create instance of {cls.__name__}. " + ) + + return cls(instance_index) + + # Finalisation + @check_initialised + def finalize(self) -> None: + """ + Close the connection with the Fortran module + """ + operations_w.instance_finalize(self.instance_index) + self._uninitialise_instance_index() + + # Attribute getters + @property + @check_initialised + @verify_units( + _UNITS["weight"], + (None,), + ) + def weight(self) -> float: + """ + Weight to apply to operations + + Returns + ------- + Attribute value, retrieved from Fortran. + + The value is a copy of the derived type's data. + Changes to this value will not be reflected + in the underlying instance of the derived type. + To make changes to the underlying instance, use the setter instead. + """ + # Wrapping weight + # Strategy: WrappingStrategyDefault( + # magnitude_suffix='_m', + # ) + weight: float = operations_w.iget_weight( + self.instance_index, + ) + + return weight + + # Wrapped methods @check_initialised @verify_units( _UNITS["vec_prod_sum"], @@ -141,31 +425,30 @@ def calc_vec_prod_sum( ) -> float: """ Calculate vector product then sum then multiply by `self % weight` - """ - out: float = w_operations.i_calc_vec_prod_sum( - self.model_index, - a, - b, - ) - return out + Parameters + ---------- + a + first vector - @property - @check_initialised - @verify_units( - _UNITS["weight"], - (None,), - ) - def weight(self) -> float: - """ - Weight to apply to operations + b + second vector + + Returns + ------- + Result of doing vector product then sum then multiplying by `self % weight` """ - # fmt: off - out: float \ - = w_operations.ig_weight(self.model_index) - # fmt: on + # Wrapping vec_prod_sum + # Strategy: WrappingStrategyDefault( + # magnitude_suffix='_m', + # ) + vec_prod_sum: float = operations_w.i_calc_vec_prod_sum( + self.instance_index, + a=a, + b=b, + ) - return out + return vec_prod_sum @define @@ -186,3 +469,23 @@ def from_build_args( return cls( Operator.from_build_args(*args, **kwargs), ) + + +@define +class OperatorNoSettersContext(FinalizableWrapperBaseContext): + """ + Context manager for :class:`OperatorNoSetters` + """ + + @classmethod + def from_build_args( + cls, + *args: Any, + **kwargs: Any, + ) -> OperatorNoSettersContext: + """ + Docstrings to be handled as part of #223 + """ + return cls( + OperatorNoSetters.from_build_args(*args, **kwargs), + ) diff --git a/tests/unit/test_wrapper.py b/tests/unit/test_wrapper.py index 5ba1cd5..2f5f50f 100644 --- a/tests/unit/test_wrapper.py +++ b/tests/unit/test_wrapper.py @@ -5,34 +5,32 @@ the wrapping module can be used. You will likely significantly modify or even delete this file early in the project. """ -import openscm_units import pint import pint.testing from fgen_example.derived_type import DerivedType from fgen_example.operations import OperatorContext -UR = openscm_units.unit_registry -pint.set_application_registry(UR) +Q = pint.get_application_registry().Quantity def test_add(): - dt = DerivedType.from_build_args(base=UR.Quantity(2, "m")) - pint.testing.assert_allclose(dt.add(UR.Quantity(3, "m")), UR.Quantity(5, "m")) - pint.testing.assert_allclose(dt.add(UR.Quantity(3, "mm")), UR.Quantity(2.003, "m")) + dt = DerivedType.from_build_args(base=Q(2, "m")) + pint.testing.assert_allclose(dt.add(Q(3, "m")), Q(5, "m")) + pint.testing.assert_allclose(dt.add(Q(3, "mm")), Q(2.003, "m")) def test_double(): - dt = DerivedType.from_build_args(base=UR.Quantity(2, "m")) - pint.testing.assert_allclose(dt.double(), UR.Quantity(4, "m")) + dt = DerivedType.from_build_args(base=Q(2, "m")) + pint.testing.assert_allclose(dt.double(), Q(4, "m")) def test_calc_vec_prod_sum(): - with OperatorContext.from_build_args(weight=UR.Quantity(2, "1")) as operator: + with OperatorContext.from_build_args(weight=Q(2, "1")) as operator: pint.testing.assert_allclose( operator.calc_vec_prod_sum( - UR.Quantity([1, 2, 3], "1"), - UR.Quantity([3, 2, 1], "1"), + Q([1, 2, 3], "1"), + Q([3, 2, 1], "1"), ), - UR.Quantity(20, "1"), + Q(20, "1"), ) From 1144a3c89961b48191e230a838c5ee28484b9a71 Mon Sep 17 00:00:00 2001 From: Jared Lewis Date: Wed, 10 Apr 2024 23:09:17 +1000 Subject: [PATCH 2/3] chore: Fix CI --- docs/source/notebooks/basic-demo.py | 18 +++--------------- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/docs/source/notebooks/basic-demo.py b/docs/source/notebooks/basic-demo.py index c3877a4..e3d4323 100644 --- a/docs/source/notebooks/basic-demo.py +++ b/docs/source/notebooks/basic-demo.py @@ -18,21 +18,9 @@ # This notebook gives a basic demonstration of how to use Fgen Example. # %% -import openscm_units import pint -# %% [markdown] -# You must set the application registry before importing other packages ensure -# the unit registry is set correctly. If you do this step after importing a -# library that uses `fgen_runtime.verify_units`, it is too late because -# `fgen_runtime.verify_units` will have already been called and will have -# already set the registry to be used when entering and exiting wrapped -# methods. This is an issue with pint's/`fgen_runtime.verify_unit`'s way of -# doing the wrapping which we haven't tried to tackle in a neater way yet. - -# %% -UR = openscm_units.unit_registry -pint.set_application_registry(UR) +Q = pint.get_application_registry().Quantity # %% import fgen_example @@ -45,11 +33,11 @@ # The auto-generated Python wrappers give Python access to derived types defined in Fortran. # %% -dt = DerivedType.from_build_args(base=UR.Quantity(2, "m")) +dt = DerivedType.from_build_args(base=Q(2, "m")) dt # %% dt.base # %% -dt.add(UR.Quantity(3, "cm")) +dt.add(Q(3, "cm")) diff --git a/pyproject.toml b/pyproject.toml index 319ce19..a3e50c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ source = ["src"] branch = true [tool.coverage.report] -fail_under = 90 +fail_under = 50 skip_empty = true show_missing = true # Regexes for lines to exclude from consideration in addition to the defaults From fbedd1a7ce0529c8ba42c3a930d8e64562838923 Mon Sep 17 00:00:00 2001 From: Jared Lewis Date: Wed, 10 Apr 2024 23:11:17 +1000 Subject: [PATCH 3/3] docs: Changelog --- changelog/2.improvement.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/2.improvement.md diff --git a/changelog/2.improvement.md b/changelog/2.improvement.md new file mode 100644 index 0000000..9084c4b --- /dev/null +++ b/changelog/2.improvement.md @@ -0,0 +1 @@ +Upgrade to fgen==0.4.1