diff --git a/.fprettify.rc b/.fprettify.rc new file mode 100644 index 0000000..6d4c29c --- /dev/null +++ b/.fprettify.rc @@ -0,0 +1,2 @@ +indent=4 +whitespace=4 diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 4fd0ef1..564cac2 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -26,4 +26,4 @@ runs: shell: bash if: ${{ (inputs.run-uv-install == 'true') }} run: | - uv sync ${{ inputs.uv-dependency-install-flags }} + uv sync --no-editable ${{ inputs.uv-dependency-install-flags }} diff --git a/.github/workflows/bump.yaml b/.github/workflows/bump.yaml index 0b6279a..c15df7d 100644 --- a/.github/workflows/bump.yaml +++ b/.github/workflows/bump.yaml @@ -56,7 +56,7 @@ jobs: echo "Bumping to version $NEW_VERSION" # Build CHANGELOG - uv run towncrier build --yes --version v$NEW_VERSION + uv run --no-sync towncrier build --yes --version v$NEW_VERSION # Commit, tag and push git commit -a -m "bump: version $BASE_VERSION -> $NEW_VERSION" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ea7a499..79f9590 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -23,7 +23,7 @@ jobs: uv-dependency-install-flags: "--all-extras --group dev" - name: mypy run: | - MYPYPATH=stubs uv run mypy src + MYPYPATH=stubs uv run --no-sync mypy src docs: if: ${{ !github.event.pull_request.draft }} @@ -41,7 +41,7 @@ jobs: uv-dependency-install-flags: "--all-extras --group docs" - name: docs run: | - uv run mkdocs build --strict + uv run --no-sync mkdocs build --strict - uses: ./.github/actions/setup with: python-version: "3.11" @@ -49,8 +49,8 @@ jobs: - name: docs-with-changelog run: | # Check CHANGELOG will build too - uv run towncrier build --yes - uv run mkdocs build --strict + uv run --no-sync towncrier build --yes + uv run --no-sync mkdocs build --strict # Just in case, undo the staged changes git restore --staged . && git restore . @@ -65,7 +65,9 @@ jobs: with: # Exclude local links # and the template link in pyproject.toml - args: "--exclude 'file://' --exclude '^https://github\\.com/openscm/example-fgen-basic/pull/\\{issue\\}$' ." + # Don't check for conda-forge while we haven't released there + # Don't check https://www.readthedocs.org/ as it hits rate limits + args: "--exclude 'file://' --exclude '^https://github\\.com/openscm/example-fgen-basic/pull/\\{issue\\}$' --exclude '^https://github.com/conda-forge/example-fgen-basic-feedstock$' --exclude '^https://www.readthedocs.org/$' ." tests: strategy: @@ -97,8 +99,10 @@ jobs: uv-dependency-install-flags: "--all-extras --group tests" - name: Run tests run: | - uv run pytest -r a -v src tests --doctest-modules --cov=src --cov-report=term-missing --cov-report=xml - uv run coverage report + + COV_DIR=`uv run --no-sync python -c 'from pathlib import Path; import example_fgen_basic; print(Path(example_fgen_basic.__file__).parent)'` + uv run --no-sync pytest -r a -v tests src --doctest-modules --doctest-report ndiff --cov=${COV_DIR} --cov-report=term-missing --cov-report=xml + uv run --no-sync coverage report - name: Upload coverage reports to Codecov with GitHub Action uses: codecov/codecov-action@v4.2.0 env: @@ -220,15 +224,13 @@ jobs: steps: - name: Check out repository uses: actions/checkout@v4 - - name: Setup uv - id: setup-uv - uses: astral-sh/setup-uv@v4 + - uses: ./.github/actions/setup with: - version: "0.8.8" python-version: ${{ matrix.python-version }} + uv-dependency-install-flags: "--group dev" - name: Build package run: | - uv run python scripts/add-locked-targets-to-pyproject-toml.py + uv run --no-sync python scripts/add-locked-targets-to-pyproject-toml.py cat pyproject.toml uv build # Just in case, undo the changes to `pyproject.toml` @@ -256,5 +258,5 @@ jobs: run: | TEMP_FILE=$(mktemp) uv export --no-dev > $TEMP_FILE - uv run liccheck -r $TEMP_FILE -R licence-check.txt + uv run --no-sync liccheck -r $TEMP_FILE -R licence-check.txt cat licence-check.txt diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index e31361c..98af6ed 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -36,7 +36,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Publish to PyPI run: | - uv run python scripts/add-locked-targets-to-pyproject-toml.py + uv run --no-sync python scripts/add-locked-targets-to-pyproject-toml.py uv build uv publish # Just in case, undo the changes to `pyproject.toml` diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c57e715..4bcb670 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -33,7 +33,7 @@ jobs: echo "PROJECT_VERSION=$PROJECT_VERSION" >> $GITHUB_ENV - name: Build package for PyPI run: | - uv run python scripts/add-locked-targets-to-pyproject-toml.py + uv run --no-sync python scripts/add-locked-targets-to-pyproject-toml.py uv build # Just in case, undo the changes to `pyproject.toml` git restore --staged . && git restore . @@ -43,7 +43,7 @@ jobs: echo "## Changelog" >> ".github/release_template.md" echo "" >> ".github/release_template.md" uv add typer - uv run python scripts/changelog-to-release-template.py >> ".github/release_template.md" + uv run --no-sync python scripts/changelog-to-release-template.py >> ".github/release_template.md" echo "" >> ".github/release_template.md" echo "## Changes" >> ".github/release_template.md" echo "" >> ".github/release_template.md" diff --git a/.gitignore b/.gitignore index 4577fd0..4feda63 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,29 @@ +# Local install dir +install-example + +# Cloned dependencies +subprojects/test-drive + # Notebooks *.ipynb # Auto-generated docs and helper files docs/api/* !docs/api/.gitkeep - -# pdm stuff -.pdm-python +docs/fgen/api/* +!docs/fgen/api/.gitkeep +docs/fgen-runtime/api/* +!docs/fgen-runtime/api/.gitkeep # Databases *.db +# Downloaded wheels +*.whl + +# cmake +cmake-build-* + # Jupyter cache .jupyter_cache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 344ff96..79a2813 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,12 @@ ci: autoupdate_schedule: quarterly autoupdate_branch: pre-commit-autoupdate # Currently network access isn't supported in the pre-commit CI product. - skip: [uv-sync, uv-lock, uv-export] + skip: [ + propagate-pyproject-metadata, + uv-sync, + uv-lock, + uv-export, + ] # See https://pre-commit.com/hooks.html for more hooks repos: @@ -63,3 +68,24 @@ repos: args: ["-o", "requirements-only-tests-locked.txt", "--no-hashes", "--no-dev", "--no-emit-project", "--only-group", "tests"] # # Not released yet # - id: uv-sync + - repo: local + hooks: + - id: propagate-pyproject-metadata + name: propagate-pyproject-metadata + entry: uv run --no-sync python scripts/propogate-pyproject-metadata.py + files: | + (?x)^( + meson.build| + scripts/propogate-pyproject-metadata.py| + pyproject.toml + )$ + language: system + require_serial: true + pass_filenames: false + - id: inject-srcs-into-meson-build + name: inject-srcs-into-meson-build + entry: uv run --no-sync python scripts/inject-srcs-into-meson-build.py + files: \.(py|f90|build)$ + language: system + require_serial: true + pass_filenames: false diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 9cc8034..8c9d143 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -9,11 +9,10 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.11" + python: "mambaforge-22.9" jobs: post_install: - # RtD seems to be not happy with pdm installs, - # hence use pip directly instead. + - which python - python -m pip install -r requirements-docs-locked.txt - python -m pip list pre_build: @@ -22,3 +21,6 @@ build: mkdocs: configuration: mkdocs.yml fail_on_warning: true + +conda: + environment: environment-docs-conda-base.yml diff --git a/Makefile b/Makefile index 5382d7b..94e8b82 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,10 @@ # Will likely fail on Windows, but Makefiles are in general not Windows # compatible so we're not too worried TEMP_FILE := $(shell mktemp) +# Directory in which to build the Fortran when using a standalone build +BUILD_DIR := build +# Coverage directory - needed to trick code cov to look in the right place +COV_DIR := $(shell uv run --no-sync python -c 'from pathlib import Path; import example_fgen_basic; print(Path(example_fgen_basic.__file__).parent)') # A helper script to get short descriptions of each target in the Makefile define PRINT_HELP_PYSCRIPT @@ -24,8 +28,8 @@ help: ## print short description of each target .PHONY: checks checks: ## run all the linting checks of the codebase - @echo "=== pre-commit ==="; uv run pre-commit run --all-files || echo "--- pre-commit failed ---" >&2; \ - echo "=== mypy ==="; MYPYPATH=stubs uv run mypy src || echo "--- mypy failed ---" >&2; \ + @echo "=== pre-commit ==="; uv run --no-sync pre-commit run --all-files || echo "--- pre-commit failed ---" >&2; \ + echo "=== mypy ==="; MYPYPATH=stubs uv run --no-sync mypy src || echo "--- mypy failed ---" >&2; \ echo "======" .PHONY: ruff-fixes @@ -37,8 +41,17 @@ ruff-fixes: ## fix the code using ruff uv run ruff format src tests scripts docs .PHONY: test -test: ## run the tests - uv run pytest src tests -r a -v --doctest-modules --doctest-report ndiff --cov=src +test: ## run the tests (re-installs the package every time so you might want to run by hand if you're certain that step isn't needed) + # Note: passing `src` to pytest causes the `src` directory to be imported + # if the package has not already been installed. + # This is a problem, as the package is not importable from `src` by itself because the extension module is not available. + # As a result, you have to pass `pytest tests src` rather than `pytest src tests` + # to ensure that the package is imported from the venv and not `src`. + # The issue with this is that code coverage then doesn't work, + # because it is looking for lines in `src` to be run, + # but they're not because lines in `.venv` are run instead. + # We don't have a solution to this yet. + uv run --no-editable --reinstall-package example-fgen-basic pytest -r a -v tests src --doctest-modules --doctest-report ndiff --cov=$(COV_DIR) # Note on code coverage and testing: # You must specify cov=src. @@ -55,15 +68,15 @@ test: ## run the tests .PHONY: docs docs: ## build the docs - uv run mkdocs build + uv run --no-sync mkdocs build .PHONY: docs-strict docs-strict: ## build the docs strictly (e.g. raise an error on warnings, this most closely mirrors what we do in the CI) - uv run mkdocs build --strict + uv run --no-sync mkdocs build --strict .PHONY: docs-serve docs-serve: ## serve the docs locally - uv run mkdocs serve + uv run --no-sync mkdocs serve .PHONY: changelog-draft changelog-draft: ## compile a draft of the next changelog @@ -79,5 +92,30 @@ licence-check: ## Check that licences of the dependencies are suitable .PHONY: virtual-environment virtual-environment: ## update virtual environment, create a new one if it doesn't already exist - uv sync --all-extras --group all-dev - uv run pre-commit install + uv sync --no-editable --all-extras --group all-dev + uv run --no-sync pre-commit install + +.PHONY: format-fortran +format-fortran: ## format the Fortran files + uv run fprettify -r src -c .fprettify.rc + +$(BUILD_DIR): # setup the standlone Fortran build directory + uv run meson setup $(BUILD_DIR) + +.PHONY: build-fortran +build-fortran: | $(BUILD_DIR) ## build/compile the Fortran (including the extension module) + uv run meson compile -C build -v + +.PHONY: test-fortran +test-fortran: build-fortran ## run the Fortran tests + uv run meson test -C build -v + +.PHONY: install-fortran +install-fortran: build-fortran ## install the Fortran (including the extension module) + uv run meson install -C build -v + # # Can also do this to see where things go without making a mess + # uv run meson install -C build --destdir ../install-example + +.PHONY: clean +clean: ## clean all build artefacts + rm -rf $(BUILD_DIR) diff --git a/changelog/2.feature.md b/changelog/2.feature.md new file mode 100644 index 0000000..9d3df13 --- /dev/null +++ b/changelog/2.feature.md @@ -0,0 +1 @@ +Add basic functionality that wraps Fortran diff --git a/config/install-mod.py b/config/install-mod.py new file mode 100644 index 0000000..516887b --- /dev/null +++ b/config/install-mod.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +""" +Originally copied from: https://github.com/toml-f/toml-f/blob/main/config/install-mod.py + +Using the MIT License, copyright notice below +(from https://github.com/toml-f/toml-f/blob/main/LICENSE-MIT) + +Copyright (c) 2019-2021 Sebastian Ehlert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +""" + +from os import environ, listdir, makedirs +from os.path import exists, isdir, join +from shutil import copy +from sys import argv + +build_dir = environ["MESON_BUILD_ROOT"] + +if "MESON_INSTALL_DESTDIR_PREFIX" in environ: + install_dir = environ["MESON_INSTALL_DESTDIR_PREFIX"] + +else: + install_dir = environ["MESON_INSTALL_PREFIX"] + +include_dir = argv[1] if len(argv) > 1 else "include" +module_dir = join(install_dir, include_dir) + +modules = [] +for d in listdir(build_dir): + bd = join(build_dir, d) + if isdir(bd): + for f in listdir(bd): + if f.endswith(".mod"): + modules.append(join(bd, f)) + +if not exists(module_dir): + makedirs(module_dir) + +for mod in modules: + print("Installing (from custom python script)", mod, "to", module_dir) + copy(mod, module_dir) diff --git a/config/meson.build b/config/meson.build new file mode 100644 index 0000000..dedeced --- /dev/null +++ b/config/meson.build @@ -0,0 +1,25 @@ +os = host_machine.system() + +if os == 'windows' + add_project_link_arguments( + '-Wl,--allow-multiple-definition', + language: 'fortran', + ) +endif + +fc = meson.get_compiler('fortran') +fc_id = fc.get_id() + +if fc_id == 'gcc' + # from previous setup: "-g -O0 -Wall -fimplicit-none -fcheck=all,no-recursion + # -fbacktrace -Wno-unused-dummy-argument -Wno-unused-function" + add_project_arguments( + '-fbacktrace', + '-fcheck=all,no-recursion', + '-ffree-line-length-none', + '-fimplicit-none', + '-Wno-unused-dummy-argument', + '-Wno-unused-function', + language: 'fortran', + ) +endif diff --git a/docs/gen_doc_stubs.py b/docs/gen_doc_stubs.py index df22601..d8a92b3 100644 --- a/docs/gen_doc_stubs.py +++ b/docs/gen_doc_stubs.py @@ -48,6 +48,9 @@ def write_subpackage_pages(subpackage: object) -> tuple[PackageInfo, ...]: for _, name, is_pkg in pkgutil.walk_packages(subpackage.__path__): subpackage_full_name = subpackage.__name__ + "." + name sub_package_info = write_package_page(subpackage_full_name) + if sub_package_info is None: + continue + sub_sub_packages.append(sub_package_info) return tuple(sub_sub_packages) @@ -66,7 +69,7 @@ def get_write_file(package_full_name: str) -> Path: def write_package_page( package_full_name: str, -) -> PackageInfo: +) -> PackageInfo | None: """ Write the docs pages for a package (or sub-package) """ @@ -76,6 +79,8 @@ def write_package_page( write_subpackage_pages(package) package_name = package_full_name.split(".")[-1] + if package_name == "_lib": + return None write_file = get_write_file(package_full_name) diff --git a/environment-docs-conda-base.yml b/environment-docs-conda-base.yml new file mode 100644 index 0000000..0d2d79b --- /dev/null +++ b/environment-docs-conda-base.yml @@ -0,0 +1,11 @@ +name: fortran-compilers + +channels: + - conda-forge + - defaults + +dependencies: + - python=3.11 + - pip + - gcc + - gfortran diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..c7f8970 --- /dev/null +++ b/meson.build @@ -0,0 +1,229 @@ +project( + 'example-fgen-basic', # injected with scripts/make-version-consistent.py, don't edit by hand + ['c', 'fortran'], # C as f2py creates C files so we need a C compiler + # Dev version (i.e. anything that isn't a release) will be 0.0.0 + # because meson doesn't support pre-release identifiers like `.a`, `.post` + version: '0.0.0', # injected with scripts/make-version-consistent.py, don't edit by hand + license: 'BSD-3-Clause', + meson_version: '>=1.3.0', + default_options: [ + 'buildtype=debugoptimized', + 'default_library=both', + ], +) + +# Some useful constants +pyprojectwheelbuild_enabled = get_option('pyprojectwheelbuild').enabled() + +# Print out values if you want +# To set this, you have to run e.g. +# `uv run meson configure -Dverbose=0` +# first, then run all the compile and other steps +message('verbose: ', get_option('verbose')) +if get_option('verbose') > 0 + message('default_library', get_option('default_library')) + message('pyprojectwheelbuild.enabled(): ', pyprojectwheelbuild_enabled) + message('soversion', get_option('soversion')) +endif + +if pyprojectwheelbuild_enabled + ## Python wrapper + python_project_name = 'example_fgen_basic' # injected with scripts/make-version-consistent.py, don't edit by hand + extension_module_name = '_lib' # Doesn't need to be more specific I don't think (?) + + if get_option('verbose') > 0 + message('python_project_name: ', python_project_name) + message('extension_module_name: ', extension_module_name) + endif + + # Not actually required. + # Useful to have to improve error messages + # in the case where there is no Fortran compiler + fc = meson.get_compiler('fortran') + + py = import('python').find_installation(pure: false) + py_dep = py.dependency() + + # Specify all the wrapper Fortran files. + # These are the src with which Python interacts. + # Injected with `script/inject-srcs-into-meson-build.py` + srcs = files( + 'src/example_fgen_basic/get_wavelength_wrapper.f90', + ) + + # Specify all the other source Fortran files (original files and managers) + # Injected with `script/inject-srcs-into-meson-build.py` + srcs_ancillary_lib = files( + 'src/example_fgen_basic/get_wavelength.f90', + 'src/example_fgen_basic/kind_parameters.f90', + ) + + # All Python files (wrappers and otherwise) + # Injected with `script/inject-srcs-into-meson-build.py` + python_srcs = files( + 'src/example_fgen_basic/__init__.py', + 'src/example_fgen_basic/exceptions.py', + 'src/example_fgen_basic/get_wavelength.py', + 'src/example_fgen_basic/operations.py', + 'src/example_fgen_basic/runtime_helpers.py', + ) + + # The ancillary library, + # i.e. all the stuff for wrapping that isn't directly exposed to Python. + ancillary_lib = library( + '@0@-ancillary'.format(meson.project_name()), + sources: srcs_ancillary_lib, + version: meson.project_version(), + dependencies: [], + # any other dependencies which aren't in source e.g. fgen-core + # e.g. dependencies: [fgen_core_dep], + install: false, + ) + + ancillary_dep = declare_dependency(link_with: ancillary_lib) + + # Get numpy and f2py headers and setup dependencies + incdir_numpy = run_command(py, + ['-c', 'import os; import numpy; print(numpy.get_include())'], + check : true + ).stdout().strip() + + incdir_f2py = run_command(py, + ['-c', 'import os; import numpy.f2py; print(numpy.f2py.get_include())'], + check : true + ).stdout().strip() + + inc_np = include_directories(incdir_numpy) + np_dep = declare_dependency(include_directories: inc_np) + + # Run f2py to generate the Python-Fortran interface + python_fortran_interface = custom_target( + 'f2py_extension_module', + input: srcs, + output: [ + # Naming controlled by f2py here hence slightly random use of hyphens + '@0@module.c'.format(extension_module_name), + '@0@-f2pywrappers2.f90'.format(extension_module_name), + ], + command: [ + py, + '-m', + 'numpy.f2py', + '@INPUT@', + '-m', + extension_module_name, + '--lower' + ] + ) + # Build the extension module + py.extension_module( + extension_module_name, + [srcs, python_fortran_interface], + incdir_f2py / 'fortranobject.c', + include_directories: [inc_np, incdir_f2py], + dependencies : [ancillary_dep], + # # If you need other deps, add them here too e.g. + # dependencies : [ancillary_dep, fgen_core_dep], + install: true, + subdir: python_project_name, + ) + + # If some files (such as .py files) need to be copied to site-packages, + # this is where that operation happens. + # Files get copied to /site-packages/ + foreach python_src : python_srcs + py.install_sources( + python_src, + subdir: python_project_name, + pure: false, + ) + + # TODO: check if anything needs to happen here for e.g. LICENCE, Python source files + + endforeach + +else + ## Fortran library standalone compilation + + # Copied from https://github.com/toml-f/toml-f/blob/main/meson.build + # Not clear what the right choice is here yet + install = not (meson.is_subproject() and get_option('default_library') == 'static') + + if get_option('testing').auto() + testing_enabled = not meson.is_subproject() + else + testing_enabled = get_option('testing').enabled() + endif + + if get_option('verbose') > 0 + message('install', install) + message('get_option("testing"): ', get_option('testing')) + message('meson.is_subproject(): ', meson.is_subproject()) + message('testing_enabled: ', testing_enabled) + endif + + # Purpose not 100% clear, but compiler flags look useful so following + # https://github.com/toml-f/toml-f/blob/main/meson.build for now. + # TODO: think about this more carefully. + # General configuration information + subdir('config') + + # Collect source of the Fortran dependencies + srcs = [] + subdir('src') + + # Library target + # TODO: think about whether this should be in `src/example.meson.build` + # as that nesting seems to be the more common pattern + example_lib = library( + meson.project_name(), + sources: srcs, + version: meson.project_version(), + soversion : get_option('soversion'), + install: pyprojectwheelbuild_enabled ? false : install, + ) + + # Export dependency for other projects and test suite to use + example_inc = example_lib.private_dir_include() + example_dep = declare_dependency( + link_with: example_lib, + include_directories: example_inc, + ) + + if install + + # Package the license files + example_lic = files('LICENCE') + install_data( + example_lic, + install_dir: get_option('datadir')/'licenses'/meson.project_name() + ) + + module_id = meson.project_name() / 'modules' + + # Copy .mod files into includedir + meson.add_install_script( + find_program(files('config'/'install-mod.py')), + get_option('includedir') / module_id, + ) + + # Generate package config + # https://mesonbuild.com/Pkgconfig-module.html + # https://people.freedesktop.org/~dbn/pkg-config-guide.html + pkg = import('pkgconfig') + pkg.generate( + example_lib, + description: 'Basic example of using fgen. This is the standalone Fortran library.', # injected with scripts/make-version-consistent.py, do not edit by hand + subdirs: ['', module_id], + ) + + # TODO: check if anything needs to happen here for e.g. LICENCE, something else + + endif + + if testing_enabled + # add the Fortran testsuite + subdir('tests') + endif + +endif diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..f6e8a3c --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,28 @@ +option( + 'pyprojectwheelbuild', + type: 'feature', + value: 'auto', + description: 'Enable building of the wheel via `pyproject.toml`', +) + +option( + 'soversion', + type: 'string', + value: '1', + description: 'Shared object version', +) + +option( + 'testing', + type: 'feature', + value: 'auto', + description: 'Enable testing of example"s Fortran library', +) + +option( + 'verbose', + type: 'integer', + value: 1, + min: 0, + description: 'Verbosity of the build (i.e. control how many messages are shown)', +) diff --git a/pyproject.toml b/pyproject.toml index 2b3c23b..0d6e8a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,10 @@ authors = [ license = { text = "placeholder" } requires-python = ">=3.9" dependencies = [ + "attrs>=24.3.0", + "numpy>=1.26.0; python_version < '3.13'", + "numpy>=2.1.0; python_version >= '3.13'", + "typing-extensions>=4.12.2", ] readme = "README.md" classifiers = [ @@ -56,195 +60,52 @@ full = [ dev = [ # Key dependencies # ---------------- - "liccheck==0.9.2", - "mypy==1.14.0", + "fprettify>=0.3.7", + "liccheck>=0.9.2", + "mypy>=1.14.0", + "pint>=0.24.4", # Required for liccheck, see https://github.com/dhatim/python-license-check/pull/113 - "pip==24.3.1", - "pre-commit==4.0.1", + "pip>=24.3.1", + "pre-commit>=4.0.1", # Required for liccheck, see https://github.com/dhatim/python-license-check/pull/113 - "setuptools==75.6.0", - "towncrier==24.8.0", - "tomli-w==1.2.0", - "tomli==2.2.1", - "typer==0.15.2", - # Implied by the key dependencies above - # ------------------------------------- - "cfgv==3.4.0", - "click==8.1.8", - "colorama==0.4.6 ; sys_platform == 'win32'", - "distlib==0.3.9", - "filelock==3.16.1", - "identify==2.6.5", - "jinja2==3.1.5", - "markupsafe==3.0.2", - "mypy-extensions==1.0.0", - "nodeenv==1.9.1", - "platformdirs==4.3.6", - "pyyaml==6.0.2", - "semantic-version==2.10.0", - "toml==0.10.2", - "typing-extensions==4.12.2", - "virtualenv==20.28.1", - "markdown-it-py==3.0.0", - "mdurl==0.1.2", - "pygments==2.19.1", - "rich==13.9.4", - "shellingham==1.5.4", + "setuptools>=75.6.0", + "towncrier>=24.8.0", + "tomli-w>=1.2.0", + "tomli>=2.2.1", + "typer>=0.15.2", ] docs = [ # Key dependencies # ---------------- - "attrs==25.3.0", - "mkdocs-autorefs==1.4.2", - "mkdocs-gen-files==0.5.0", - "mkdocs-literate-nav==0.6.2", - "mkdocs-material==9.6.16", - "mkdocs-section-index==0.3.10", - "mkdocs==1.6.1", - "mkdocstrings-python-xref==1.16.3", - "mkdocstrings-python==1.16.12", - "pymdown-extensions==10.16.1", - "ruff==0.12.8", - # Implied by the key dependencies above - # ------------------------------------- - "babel==2.16.0", - "backrefs==5.9", - "certifi==2024.12.14", - "charset-normalizer==3.4.1", - "click==8.1.8", - "colorama==0.4.6", - "ghp-import==2.1.0", - "griffe==1.11.0", - "idna==3.10", - "jinja2==3.1.5", - "markdown==3.7", - "markupsafe==3.0.2", - "mergedeep==1.3.4", - "mkdocs-get-deps==0.2.0", - "mkdocs-material-extensions==1.3.1", - "mkdocstrings==0.30.0", - "packaging==24.2", - "paginate==0.5.7", - "pathspec==0.12.1", - "platformdirs==4.3.6", - "pygments==2.19.1", - "python-dateutil==2.9.0.post0", - "pyyaml-env-tag==0.1", - "pyyaml==6.0.2", - "requests==2.32.3", - "six==1.17.0", - "urllib3==2.3.0", - "watchdog==6.0.0", + "attrs>=25.3.0", + "mkdocs-autorefs>=1.4.2", + "mkdocs-gen-files>=0.5.0", + "mkdocs-literate-nav>=0.6.2", + "mkdocs-material>=9.6.16", + "mkdocs-section-index>=0.3.10", + "mkdocs>=1.6.1", + "mkdocstrings-python-xref>=1.16.3", + "mkdocstrings-python>=1.16.12", + "pymdown-extensions>=10.16.1", + "ruff>=0.12.8", # Key dependencies for notebook_based_docs # ---------------------------------------- - "jupyterlab==4.4.5", - "jupytext==1.17.2", - "mkdocs-jupyter==0.25.1", - # Implied by the key dependencies above - # ------------------------------------- - "anyio==4.8.0", - "appnope==0.1.4 ; sys_platform == 'darwin'", - "argon2-cffi-bindings==21.2.0", - "argon2-cffi==23.1.0", - "arrow==1.3.0", - "asttokens==3.0.0", - "async-lru==2.0.4", - "beautifulsoup4==4.12.3", - "bleach==6.2.0", - "cffi==1.17.1", - "comm==0.2.2", - "debugpy==1.8.11", - "decorator==5.1.1", - "defusedxml==0.7.1", - "executing==2.1.0", - "fastjsonschema==2.21.1", - "fqdn==1.5.1", - "h11==0.14.0", - "httpcore==1.0.7", - "httpx==0.28.1", - "ipykernel==6.29.5", - "isoduration==20.11.0", - "jedi==0.19.2", - "json5==0.10.0", - "jsonpointer==3.0.0", - "jsonschema-specifications==2024.10.1", - "jsonschema==4.23.0", - "jupyter-client==8.6.3", - "jupyter-core==5.7.2", - "jupyter-events==0.11.0", - "jupyter-lsp==2.2.5", - "jupyter-server-terminals==0.5.3", - "jupyter-server==2.15.0", - "jupyterlab-pygments==0.3.0", - "jupyterlab-server==2.27.3", - "markdown-it-py==3.0.0", - "matplotlib-inline==0.1.7", - "mdit-py-plugins==0.4.2", - "mdurl==0.1.2", - "mistune==3.0.2", - "nbclient==0.10.2", - "nbconvert==7.16.4", - "nbformat==5.10.4", - "nest-asyncio==1.6.0", - "notebook-shim==0.2.4", - "overrides==7.7.0", - "pandocfilters==1.5.1", - "parso==0.8.4", - "prometheus-client==0.21.1", - "prompt-toolkit==3.0.48", - "psutil==6.1.1", - "pure-eval==0.2.3", - "pycparser==2.22", - "python-json-logger==3.2.1", - "pywin32==308 ; platform_python_implementation != 'PyPy' and sys_platform == 'win32'", - "pywinpty==2.0.14 ; os_name == 'nt'", - "pyzmq==26.2.0", - "referencing==0.35.1", - "rfc3339-validator==0.1.4", - "rfc3986-validator==0.1.1", - "rpds-py==0.22.3", - "send2trash==1.8.3", - "setuptools==75.6.0", - "sniffio==1.3.1", - "soupsieve==2.6", - "stack-data==0.6.3", - "terminado==0.18.1", - "tinycss2==1.4.0", - "tornado==6.4.2", - "traitlets==5.14.3", - "types-python-dateutil==2.9.0.20241206", - "uri-template==1.3.0", - "wcwidth==0.2.13", - "webcolors==24.11.1", - "webencodings==0.5.1", - "websocket-client==1.8.0", + "jupyterlab>=4.4.5", + "jupytext>=1.17.2", + "mkdocs-jupyter>=0.25.1", ] # For minimum test dependencies. # These are used when running our minimum PyPI install tests. tests-min = [ # Key dependencies # ---------------- - "pytest==8.3.4", - # Implied by the key dependencies above - # ------------------------------------- - "colorama==0.4.6 ; sys_platform == 'win32'", - "iniconfig==2.0.0", - "packaging==24.2", - "pluggy==1.5.0", + "pytest>=8.3.4", ] # Full test dependencies. tests-full = [ # Key dependencies # ---------------- - "pytest-cov==6.0.0", - # Implied by the key dependencies above - # ------------------------------------- - "colorama==0.4.6 ; sys_platform == 'win32'", - "coverage==7.6.10", - "iniconfig==2.0.0", - "packaging==24.2", - "pluggy==1.5.0", - "pytest==8.3.4", + "pytest-cov>=6.0.0", ] # Test dependencies # (partly split because liccheck uses toml, @@ -259,14 +120,22 @@ all-dev = [ {include-group = "tests"}, ] +# Start here, replace with whate we have in our example [build-system] -requires = ["uv_build>=0.8.7,<0.9.0"] -build-backend = "uv_build" +build-backend = "mesonpy" +requires = [ + "meson-python>=0.15.0", + "numpy", +] -[tool.uv.build-backend] -source-include = [ - "src/example_fgen_basic", - "LICENCE", +# https://mesonbuild.com/meson-python/how-to-guides/meson-args.html +[tool.meson-python.args] +setup = [ + '--default-library=static', + '-Dpyprojectwheelbuild=enabled', + ] +install = [ +'--skip-subprojects', ] [tool.coverage.run] @@ -274,6 +143,11 @@ source = [ "src", ] branch = true +omit = [ + # TODO: check this file + "*exceptions.py", + "*runtime_helpers.py", +] [tool.coverage.report] fail_under = 90 diff --git a/requirements-docs-locked.txt b/requirements-docs-locked.txt index 0a17ef3..b3ea620 100644 --- a/requirements-docs-locked.txt +++ b/requirements-docs-locked.txt @@ -135,7 +135,7 @@ tomli==2.2.1 ; python_full_version < '3.11' tornado==6.4.2 traitlets==5.14.3 types-python-dateutil==2.9.0.20241206 -typing-extensions==4.12.2 ; python_full_version < '3.13' +typing-extensions==4.12.2 uri-template==1.3.0 urllib3==2.3.0 watchdog==6.0.0 diff --git a/requirements-incl-optional-locked.txt b/requirements-incl-optional-locked.txt index 45f760d..9855b1c 100644 --- a/requirements-incl-optional-locked.txt +++ b/requirements-incl-optional-locked.txt @@ -1,5 +1,6 @@ # This file was autogenerated by uv via the following command: # uv export -o requirements-incl-optional-locked.txt --no-hashes --no-dev --no-emit-project --all-extras +attrs==25.3.0 contourpy==1.3.0 ; python_full_version < '3.10' contourpy==1.3.2 ; python_full_version == '3.10.*' contourpy==1.3.3 ; python_full_version >= '3.11' @@ -18,4 +19,5 @@ pillow==11.3.0 pyparsing==3.2.3 python-dateutil==2.9.0.post0 six==1.17.0 +typing-extensions==4.12.2 zipp==3.23.0 ; python_full_version < '3.10' diff --git a/requirements-locked.txt b/requirements-locked.txt index 47e9913..3502dc3 100644 --- a/requirements-locked.txt +++ b/requirements-locked.txt @@ -1,2 +1,7 @@ # This file was autogenerated by uv via the following command: # uv export -o requirements-locked.txt --no-hashes --no-dev --no-emit-project +attrs==25.3.0 +numpy==2.0.2 ; python_full_version < '3.10' +numpy==2.2.6 ; python_full_version == '3.10.*' +numpy==2.3.2 ; python_full_version >= '3.11' +typing-extensions==4.12.2 diff --git a/scripts/inject-srcs-into-meson-build.py b/scripts/inject-srcs-into-meson-build.py new file mode 100644 index 0000000..3487b16 --- /dev/null +++ b/scripts/inject-srcs-into-meson-build.py @@ -0,0 +1,104 @@ +""" +Inject source files into `meson.build` + +Easier than doing this by hand and allows us to include this in pre-commit +""" + +from __future__ import annotations + +import re +import textwrap +from collections.abc import Iterable +from pathlib import Path + + +def get_src_pattern(meson_variable: str) -> str: + """ + Get the pattern to use to find the source files in `meson.build` + + Parameters + ---------- + meson_variable + Meson variable to set + + Returns + ------- + : + Pattern to use to grep for the meson variable + """ + return rf"{meson_variable} = files\(\s*('[a-z\/_\.0-9]*',\s*)*\)" + + +def get_src_substitution( + meson_variable: str, srcs: Iterable[Path], rel_to: Path +) -> str: + """ + Get the value to substitute for the meson variable + + Parameters + ---------- + meson_variable + Meson variable to set + + srcs + Sources to use for this meson variable + + rel_to + Path the sources should be relative to when setting `meson_variable` + + Returns + ------- + : + Value to use to set the meson variable + """ + inner = textwrap.indent( + ",\n".join(f"'{v.relative_to(rel_to).as_posix()}'" for v in srcs), + prefix=" " * 4, + ) + + res = f"{meson_variable} = files(\n{inner},\n )" + return res + + +def main(): + """ + Inject sources into `meson.build` + + Nicer than typing by hand + """ + REPO_ROOT = Path(__file__).parents[1] + SRC_DIR = REPO_ROOT / "src" + + srcs = [] + srcs_ancillary_lib = [] + for ffile in SRC_DIR.rglob("*.f90"): + if ffile.name.endswith("_wrapper.f90"): + srcs.append(ffile) + else: + srcs_ancillary_lib.append(ffile) + + python_srcs = tuple(SRC_DIR.rglob("*.py")) + + with open(REPO_ROOT / "meson.build") as fh: + meson_build_in = fh.read().strip() + + meson_build_out = meson_build_in + for meson_variable, src_paths in ( + ("srcs", srcs), + ("srcs_ancillary_lib", srcs_ancillary_lib), + ("python_srcs", python_srcs), + ): + pattern = get_src_pattern(meson_variable) + substitution = get_src_substitution( + meson_variable, sorted(src_paths), REPO_ROOT + ) + + meson_build_out = re.sub(pattern, substitution, meson_build_out) + + with open(REPO_ROOT / "meson.build", "w") as fh: + fh.write(meson_build_out) + fh.write("\n") + + +if __name__ == "__main__": + main() diff --git a/scripts/propogate-pyproject-metadata.py b/scripts/propogate-pyproject-metadata.py new file mode 100644 index 0000000..847c37f --- /dev/null +++ b/scripts/propogate-pyproject-metadata.py @@ -0,0 +1,54 @@ +""" +Propogate information from `pyproject.toml` to other parts of the project as needed +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import tomllib + + +def main(): + """ + Propogate information from `pyproject.toml` to the rest of the project files + """ + REPO_ROOT = Path(__file__).parents[1] + with open(REPO_ROOT / "pyproject.toml", "rb") as fh: + pyproject_toml = tomllib.load(fh) + + project_name = pyproject_toml["project"]["name"] + project_name_python = project_name.replace("-", "_") + version = pyproject_toml["project"]["version"] + if re.match(".*[a-z].*", version): + # Can't use pre-releases in meson.build, switch to dev version + version = "0.0.0" + + description = pyproject_toml["project"]["description"] + + with open(REPO_ROOT / "meson.build") as fh: + meson_build_in = fh.read().strip() + + meson_build_out = meson_build_in + for pattern, substitution in ( + (r"version\: '[0-9a-z\.]*'", f"version: '{version}'"), + ( + r"python_project_name = '[a-z\-_]*'", + f"python_project_name = '{project_name_python}'", + ), + (r"project\(\s*'[a-z\-_]*'", f"project(\n '{project_name}'"), + ( + "description: '.*'", + f"description: '{description}. This is the standalone Fortran library.'", + ), + ): + meson_build_out = re.sub(pattern, substitution, meson_build_out) + + with open(REPO_ROOT / "meson.build", "w") as fh: + fh.write(meson_build_out) + fh.write("\n") + + +if __name__ == "__main__": + main() diff --git a/src/example_fgen_basic/__init__.py b/src/example_fgen_basic/__init__.py index 2ce9e8d..64df7f1 100644 --- a/src/example_fgen_basic/__init__.py +++ b/src/example_fgen_basic/__init__.py @@ -1,5 +1,5 @@ """ -Basic example of using fgen +Example of wrapping Fortran so it can be accessed via Python """ import importlib.metadata diff --git a/src/example_fgen_basic/exceptions.py b/src/example_fgen_basic/exceptions.py new file mode 100644 index 0000000..85ca24e --- /dev/null +++ b/src/example_fgen_basic/exceptions.py @@ -0,0 +1,81 @@ +""" +Exceptions used throughout +""" + +from __future__ import annotations + +from typing import Any, Callable, Optional + + +# TODO: move this into an `fgen_runtime` package +class CompiledExtensionNotFoundError(ImportError): + """ + Raised when a compiled extension can't be imported i.e. found + """ + + def __init__(self, compiled_extension_name: str): + error_msg = f"Could not find compiled extension {compiled_extension_name!r}" + + super().__init__(error_msg) + + +class MissingOptionalDependencyError(ImportError): + """ + Raised when an optional dependency is missing + + For example, plotting dependencies like matplotlib + """ + + def __init__(self, callable_name: str, requirement: str) -> None: + """ + Initialise the error + + Parameters + ---------- + callable_name + The name of the callable that requires the dependency + + requirement + The name of the requirement + """ + error_msg = f"`{callable_name}` requires {requirement} to be installed" + super().__init__(error_msg) + + +class WrapperError(ValueError): + """ + Base exception for errors that arise from wrapper functionality + """ + + +class NotInitialisedError(WrapperError): + """ + Raised when the wrapper around the Fortran module hasn't been initialised yet + """ + + def __init__(self, instance: Any, method: Optional[Callable[..., Any]] = None): + if method: + error_msg = f"{instance} must be initialised before {method} is called" + else: + error_msg = f"instance ({instance:r}) is not initialized yet" + + super().__init__(error_msg) + + +# TODO: change or even remove this when we move to better error handling +class UnallocatedMemoryError(ValueError): + """ + Raised when we try to access memory that has not yet been allocated + + We can't always catch this error, but this is what we raise when we can. + """ + + def __init__(self, variable_name: str): + error_msg = ( + f"The memory required to access `{variable_name}` is unallocated. " + "You must allocate it before trying to access its value. " + "Unfortunately, we cannot provide more information " + "about why this memory is not yet allocated." + ) + + super().__init__(error_msg) diff --git a/src/example_fgen_basic/get_wavelength.f90 b/src/example_fgen_basic/get_wavelength.f90 new file mode 100644 index 0000000..abe43e4 --- /dev/null +++ b/src/example_fgen_basic/get_wavelength.f90 @@ -0,0 +1,36 @@ +!> Get wavelength of light +!> +!> `!>` is for documentation that appears before the thing you're documenting +!> (https://forddocs.readthedocs.io/en/stable/user_guide/project_file_options.html#predocmark). +!> `!!` is for documentation that appears after the thing you're documenting +module m_get_wavelength + + use kind_parameters, only: dp + + implicit none + private + + real(kind=dp), parameter, public :: speed_of_light = 2.99792e8_dp + !! Speed of light [m/s] + + public :: get_wavelength + +contains + + pure function get_wavelength(frequency) result(wavelength) + !! Get wavelength of light for a given frequency + ! + ! Trying with FORD style docstrings for now + ! see https://forddocs.readthedocs.io/en/stable/ + + real(kind=dp), intent(in) :: frequency + !! Frequency + + real(kind=dp) :: wavelength + !! Corresponding wavelength + + wavelength = speed_of_light / frequency + + end function get_wavelength + +end module m_get_wavelength diff --git a/src/example_fgen_basic/get_wavelength.py b/src/example_fgen_basic/get_wavelength.py new file mode 100644 index 0000000..c6127a6 --- /dev/null +++ b/src/example_fgen_basic/get_wavelength.py @@ -0,0 +1,67 @@ +""" +Get wavelength of light given its frequency + +This is what Python users use to access the Fortran. +It is been written by hand here, +but will be auto-generated in future (including docstrings). +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from example_fgen_basic.exceptions import CompiledExtensionNotFoundError + +if TYPE_CHECKING: + import pint + +try: + from example_fgen_basic._lib import m_get_wavelength_w # type: ignore +except (ModuleNotFoundError, ImportError) as exc: # pragma: no cover + raise CompiledExtensionNotFoundError("example_fgen_basic._lib") from exc + + +def get_wavelength_plain(frequency: float) -> float: + """ + Get wavelength of light using values without units (i.e. 'plain' values) + + Parameters + ---------- + frequency + Frequency for which to get the wavelength + + Returns + ------- + : + Wavelength of light for given `frequency` + """ + res: float = m_get_wavelength_w.get_wavelength(frequency) + + return res + + +def get_wavelength( + frequency: pint.registry.UnitRegistry.Quantity, +) -> pint.registry.UnitRegistry.Quantity: + """ + Get wavelength of light + + Parameters + ---------- + frequency + Frequency for which to get the wavelength + + Returns + ------- + : + Wavelength of light for given `frequency` + """ + frequency_m = frequency.to("Hz").m + + res_m = get_wavelength_plain(frequency_m) + + # Could use frequency._REGISTRY, but private, not sure how risky that would be + # Have asked here https://github.com/hgrecco/pint/issues/2207#issuecomment-3178361201 + res = frequency.__class__(res_m, "m") + + return res diff --git a/src/example_fgen_basic/get_wavelength_wrapper.f90 b/src/example_fgen_basic/get_wavelength_wrapper.f90 new file mode 100644 index 0000000..c983c12 --- /dev/null +++ b/src/example_fgen_basic/get_wavelength_wrapper.f90 @@ -0,0 +1,41 @@ +!> Wrapper for interfacing `m_get_wavelength` with python +!> +!> Written by hand here. +!> Generation to be automated in future (including docstrings of some sort). +!> One other note: +!> This function returns a standard Fortran type. +!> In future, we will want to add in returning Fortran derived types too. +!> Doing that will require adding in an extra 'manager' layer +!> so there will be one extra file compared to what we have here. +module m_get_wavelength_w ! Convention to date: just suffix wrappers with _w + + use m_get_wavelength, only: o_get_wavelength => get_wavelength + ! We won't always need the renaming trick, + ! but here we do as the wrapper function + ! and the original function should have the same name. + ! ("o_" for original) + + implicit none + private + + public :: get_wavelength + +contains + + pure function get_wavelength(frequency) result(wavelength) + + ! 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) :: frequency + !! Frequency + + real(kind=dp) :: wavelength + !! Corresponding wavelength + + wavelength = o_get_wavelength(frequency) + + end function get_wavelength + +end module m_get_wavelength_w diff --git a/src/example_fgen_basic/kind_parameters.f90 b/src/example_fgen_basic/kind_parameters.f90 new file mode 100644 index 0000000..ffc60b0 --- /dev/null +++ b/src/example_fgen_basic/kind_parameters.f90 @@ -0,0 +1,24 @@ +!> Numerical storage size parameters for real and integer values +!> See https://fortran-lang.org/learn/best_practices/floating_point/ +module kind_parameters + + implicit none + public + + !> Single precision real numbers, 6 digits, range 10⁻³⁷ to 10³⁷-1; 32 bits + integer, parameter :: sp = selected_real_kind(6, 37) + !> Double precision real numbers, 15 digits, range 10⁻³⁰⁷ to 10³⁰⁷-1; 64 bits + integer, parameter :: dp = selected_real_kind(15, 307) + !> Quadruple precision real numbers, 33 digits, range 10⁻⁴⁹³¹ to 10⁴⁹³¹-1; 128 bits + integer, parameter :: qp = selected_real_kind(33, 4931) + + !> Char length for integers, range -2⁷ to 2⁷-1; 8 bits + integer, parameter :: i1 = selected_int_kind(2) + !> Short length for integers, range -2¹⁵ to 2¹⁵-1; 16 bits + integer, parameter :: i2 = selected_int_kind(4) + !> Length of default integers, range -2³¹ to 2³¹-1; 32 bits + integer, parameter :: i4 = selected_int_kind(9) + !> Long length for integers, range -2⁶³ to 2⁶³-1; 64 bits + integer, parameter :: i8 = selected_int_kind(18) + +end module kind_parameters diff --git a/src/example_fgen_basic/meson.build b/src/example_fgen_basic/meson.build new file mode 100644 index 0000000..741b8be --- /dev/null +++ b/src/example_fgen_basic/meson.build @@ -0,0 +1,4 @@ +srcs += files( + 'get_wavelength.f90', + 'kind_parameters.f90', +) diff --git a/src/example_fgen_basic/runtime_helpers.py b/src/example_fgen_basic/runtime_helpers.py new file mode 100644 index 0000000..3249812 --- /dev/null +++ b/src/example_fgen_basic/runtime_helpers.py @@ -0,0 +1,385 @@ +""" +Runtime helpers + +These would be moved to fgen-runtime or a similar package +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Iterable +from functools import wraps +from typing import Any, Callable, TypeVar + +import attrs +from attrs import define, field +from typing_extensions import Concatenate, ParamSpec + +from example_fgen_basic.exceptions import NotInitialisedError, UnallocatedMemoryError + +# Might be needed for Python 3.9 +# from typing_extensions import Concatenate, ParamSpec + + +# TODO: move this section to formatting module + + +def get_attribute_str_value(instance: FinalisableWrapperBase, attribute: str) -> str: + """ + Get the string version of an attribute's value + + Parameters + ---------- + instance + Instance from which to get the attribute + + attribute + Attribute for which to get the value + + Returns + ------- + String version of the attribute's value, with graceful handling of errors. + """ + try: + return f"{attribute}={getattr(instance, attribute)}" + except UnallocatedMemoryError: + # TODO: change this when we move to better error handling + return f"{attribute} is unallocated" + + +def to_str(instance: FinalisableWrapperBase, exposed_attributes: Iterable[str]) -> str: + """ + Convert an instance to its string representation + + Parameters + ---------- + instance + Instance to convert + + exposed_attributes + Attributes from Fortran that the instance exposes + + Returns + ------- + String representation of the instance + """ + if not instance.initialized: + return f"Uninitialised {instance!r}" + + if not exposed_attributes: + return repr(instance) + + attribute_values = [ + get_attribute_str_value(instance, v) for v in exposed_attributes + ] + + return f"{repr(instance)[:-1]}, {', '.join(attribute_values)})" + + +def to_pretty( + instance: FinalisableWrapperBase, + exposed_attributes: Iterable[str], + p: Any, + cycle: bool, + indent: int = 4, +) -> None: + """ + Pretty-print an instance + + Parameters + ---------- + instance + Instance to convert + + exposed_attributes + Attributes from Fortran that the instance exposes + + p + Pretty printing object + + cycle + Whether the pretty printer has detected a cycle or not. + + indent + Indent to apply to the pretty printing group + """ + if not instance.initialized: + p.text(str(instance)) + return + + if not exposed_attributes: + p.text(str(instance)) + return + + with p.group(indent, f"{repr(instance)[:-1]}", ")"): + for att in exposed_attributes: + p.text(",") + p.breakable() + + p.text(get_attribute_str_value(instance, att)) + + +def add_attribute_row( + attribute_name: str, attribute_value: str, attribute_rows: list[str] +) -> list[str]: + """ + Add a row for displaying an attribute's value to a list of rows + + Parameters + ---------- + attribute_name + Attribute's name + + attribute_value + Attribute's value + + attribute_rows + Existing attribute rows + + + Returns + ------- + Attribute rows, with the new row appended + """ + attribute_rows.append( + f"{attribute_name}{attribute_value}" # noqa: E501 + ) + + return attribute_rows + + +def to_html(instance: FinalisableWrapperBase, exposed_attributes: Iterable[str]) -> str: + """ + Convert an instance to its html representation + + Parameters + ---------- + instance + Instance to convert + + exposed_attributes + Attributes from Fortran that the instance exposes + + Returns + ------- + HTML representation of the instance + """ + if not instance.initialized: + return str(instance) + + if not exposed_attributes: + return str(instance) + + instance_class_name = repr(instance).split("(")[0] + + attribute_rows: list[str] = [] + for att in exposed_attributes: + try: + att_val = getattr(instance, att) + except UnallocatedMemoryError: + # TODO: change this when we move to better error handling + att_val = "Unallocated" + attribute_rows = add_attribute_row(att, att_val, attribute_rows) + continue + + try: + att_val = att_val._repr_html_() + except AttributeError: + att_val = str(att_val) + + attribute_rows = add_attribute_row(att, att_val, attribute_rows) + + attribute_rows_for_table = "\n ".join(attribute_rows) + + css_style = """.fgen-wrap { + /*font-family: monospace;*/ + width: 540px; +} + +.fgen-header { + padding: 6px 0 6px 3px; + border-bottom: solid 1px #777; + color: #555;; +} + +.fgen-header > div { + display: inline; + margin-top: 0; + margin-bottom: 0; +} + +.fgen-basefinalizable-cls, +.fgen-basefinalizable-instance-index { + margin-left: 2px; + margin-right: 10px; +} + +.fgen-basefinalizable-cls { + font-weight: bold; + color: #000000; +}""" + + return "\n".join( + [ + "
", + " ", + "
", + "
", + f"
{instance_class_name}
", + f"
instance_index={instance.instance_index}
", # noqa: E501 + " ", + f" {attribute_rows_for_table}", + "
", + "
", + "
", + "
", + ] + ) + + +# End of stuff to move to formatting module + +INVALID_INSTANCE_INDEX: int = -1 +""" +Value used to denote an invalid ``instance_index``. + +This can occur value when a wrapper class +has not yet been initialised (connected to a Fortran instance). +""" + + +@define +class FinalisableWrapperBase(ABC): + """ + Base class for Fortran derived type wrappers + """ + + instance_index: int = field( + validator=attrs.validators.instance_of(int), + default=INVALID_INSTANCE_INDEX, + ) + """ + Model index of wrapper Fortran instance + """ + + def __str__(self) -> str: + """ + Get string representation of self + """ + return to_str( + self, + self.exposed_attributes, + ) + + def _repr_pretty_(self, p: Any, cycle: bool) -> None: + """ + Get 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: + """ + Get html representation of self + + Used by IPython notebooks and other tools + """ + return to_html( + self, + self.exposed_attributes, + ) + + @property + def initialized(self) -> bool: + """ + Is the instance initialised, i.e. connected to a Fortran instance? + """ + return self.instance_index != INVALID_INSTANCE_INDEX + + @property + @abstractmethod + def exposed_attributes(self) -> tuple[str, ...]: + """ + Attributes exposed by this wrapper + """ + ... + + # @classmethod + # @abstractmethod + # def from_new_connection(cls) -> FinalisableWrapperBase: + # """ + # Initialise by establishing a new connection with the Fortran module + # + # This requests a new model index from the Fortran module and then + # initialises a class instance + # + # Returns + # ------- + # New class instance + # """ + # ... + # + # @abstractmethod + # def finalize(self) -> None: + # """ + # Finalise the Fortran instance and set self back to being uninitialised + # + # This method resets ``self.instance_index`` back to + # ``_UNINITIALISED_instance_index`` + # + # Should be decorated with :func:`check_initialised` + # """ + # # call to Fortran module goes here when implementing + # self._uninitialise_instance_index() + + def _uninitialise_instance_index(self) -> None: + self.instance_index = INVALID_INSTANCE_INDEX + + +P = ParamSpec("P") +T = TypeVar("T") +Wrapper = TypeVar("Wrapper", bound=FinalisableWrapperBase) + + +def check_initialised( + method: Callable[Concatenate[Wrapper, P], T], +) -> Callable[Concatenate[Wrapper, P], T]: + """ + Check that the wrapper object has been initialised before executing the method + + Parameters + ---------- + method + Method to wrap + + Returns + ------- + : + Wrapped method + + Raises + ------ + InitialisationError + Wrapper is not initialised + """ + + @wraps(method) + def checked( + ref: Wrapper, + *args: P.args, + **kwargs: P.kwargs, + ) -> Any: + if not ref.initialized: + raise NotInitialisedError(ref, method) + + return method(ref, *args, **kwargs) + + return checked # type: ignore diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..000bbf8 --- /dev/null +++ b/src/meson.build @@ -0,0 +1 @@ +subdir('example_fgen_basic') diff --git a/subprojects/test-drive.wrap b/subprojects/test-drive.wrap new file mode 100644 index 0000000..f4f2591 --- /dev/null +++ b/subprojects/test-drive.wrap @@ -0,0 +1,5 @@ +[wrap-git] +directory = test-drive +url = https://github.com/fortran-lang/test-drive.git +; revision = v0.5.0 +revision = e8b7ca492c647ed384c9845d2caed04192af7d02 diff --git a/tests/meson.build b/tests/meson.build new file mode 100644 index 0000000..65318af --- /dev/null +++ b/tests/meson.build @@ -0,0 +1,2 @@ +subdir('unit') +# subdir('integration') diff --git a/tests/unit/main.f90 b/tests/unit/main.f90 new file mode 100644 index 0000000..5f4d22f --- /dev/null +++ b/tests/unit/main.f90 @@ -0,0 +1,54 @@ +!> Driver for unit testing +program tester_unit + use, intrinsic :: iso_fortran_env, only: error_unit + + use testdrive, only: run_testsuite, new_testsuite, testsuite_type, select_suite, run_selected, get_argument + use test_get_wavelength, only: collect_get_wavelength_tests + + implicit none + integer :: stat, is + character(len=:), allocatable :: suite_name, test_name + type(testsuite_type), allocatable :: testsuites(:) + character(len=*), parameter :: fmt = '("#", *(1x, a))' + + stat = 0 + + ! add new tests here + testsuites = [new_testsuite("test_get_wavelength", collect_get_wavelength_tests)] + + call get_argument(1, suite_name) + call get_argument(2, test_name) + + if (allocated(suite_name)) then + is = select_suite(testsuites, suite_name) + if (is > 0 .and. is <= size(testsuites)) then + if (allocated(test_name)) then + write (error_unit, fmt) "Suite:", testsuites(is) % name + call run_selected(testsuites(is) % collect, test_name, error_unit, stat) + if (stat < 0) then + error stop 1 + end if + else + write (error_unit, fmt) "Testing:", testsuites(is) % name + call run_testsuite(testsuites(is) % collect, error_unit, stat) + end if + else + write (error_unit, fmt) "Available testsuites" + do is = 1, size(testsuites) + write (error_unit, fmt) "-", testsuites(is) % name + end do + error stop 1 + end if + else + do is = 1, size(testsuites) + write (error_unit, fmt) "Testing:", testsuites(is) % name + call run_testsuite(testsuites(is) % collect, error_unit, stat) + end do + end if + + if (stat > 0) then + write (error_unit, '(i0, 1x, a)') stat, "test(s) failed!" + error stop 1 + end if + +end program tester_unit diff --git a/tests/unit/meson.build b/tests/unit/meson.build new file mode 100644 index 0000000..4e69e7c --- /dev/null +++ b/tests/unit/meson.build @@ -0,0 +1,31 @@ +testdrive_dep = dependency( + 'test-drive', + fallback: ['test-drive', 'testdrive_dep'], + default_options: ['default_library=static'], + static: true, +) + +test_file_stubs = [ + 'test_get_wavelength', +] + +test_srcs = files( + 'main.f90', +) +foreach test_file_stub : test_file_stubs + test_srcs += files('@0@.f90'.format(test_file_stub)) +endforeach + +unit_tester = executable( + '@0@-tester-unit'.format(meson.project_name()), + sources: test_srcs, + dependencies: [example_dep, testdrive_dep], +) + +foreach test_file_stub : test_file_stubs + test( + 'unit_@0@'.format(test_file_stub), + unit_tester, + args: [test_file_stub], + ) +endforeach diff --git a/tests/unit/test_get_wavelength.f90 b/tests/unit/test_get_wavelength.f90 new file mode 100644 index 0000000..99cba3f --- /dev/null +++ b/tests/unit/test_get_wavelength.f90 @@ -0,0 +1,48 @@ +!> Tests of get_wavelength +module test_get_wavelength + + ! How to print to stdout + use ISO_Fortran_env, only: stdout => OUTPUT_UNIT + use testdrive, only: new_unittest, unittest_type, error_type, check + + use kind_parameters, only: dp + + implicit none + private + + public :: collect_get_wavelength_tests + +contains + + subroutine collect_get_wavelength_tests(testsuite) + !> Collection of tests + type(unittest_type), allocatable, intent(out) :: testsuite(:) + + testsuite = [new_unittest("test_get_wavelength_basic", test_get_wavelength_basic)] + + end subroutine collect_get_wavelength_tests + + subroutine test_get_wavelength_basic(error) + use m_get_wavelength, only: get_wavelength + + type(error_type), allocatable, intent(out) :: error + + real(dp) :: frequency, speed_of_light + real(dp) :: res, exp + + frequency = 4.3e14_dp + speed_of_light = 3.0e8_dp + + res = get_wavelength(frequency) + + exp = speed_of_light / frequency + + ! ! How to print to stdout + ! write( stdout, '(e13.4e2)') res + ! write( stdout, '(e13.4e2)') exp + + call check(error, res, exp, thr=1.0e-3_dp, rel=.true.) + + end subroutine test_get_wavelength_basic + +end module test_get_wavelength diff --git a/tests/unit/test_get_wavelength.py b/tests/unit/test_get_wavelength.py new file mode 100644 index 0000000..c75ac87 --- /dev/null +++ b/tests/unit/test_get_wavelength.py @@ -0,0 +1,24 @@ +""" +Dummy tests +""" + +import numpy as np +import pytest + +from example_fgen_basic.get_wavelength import get_wavelength, get_wavelength_plain + + +def test_plain(): + np.testing.assert_allclose(get_wavelength_plain(400.0e12), 749.48e-9) + + +def test_units(): + pint = pytest.importorskip("pint") + pint_testing = pytest.importorskip("pint.testing") + + ur = pint.get_application_registry() + + pint_testing.assert_allclose( + get_wavelength(ur.Quantity(400.0, "THz")).to("nm"), + ur.Quantity(749.48, "nm"), + ) diff --git a/uv.lock b/uv.lock index d10ee38..70a2632 100644 --- a/uv.lock +++ b/uv.lock @@ -2,10 +2,12 @@ version = 1 revision = 3 requires-python = ">=3.9" resolution-markers = [ - "python_full_version >= '3.11' and sys_platform == 'win32'", + "python_full_version >= '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", "python_full_version == '3.10.*' and sys_platform == 'win32'", "python_full_version < '3.10' and sys_platform == 'win32'", - "python_full_version >= '3.11' and sys_platform != 'win32'", + "python_full_version >= '3.13' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'win32'", "python_full_version == '3.10.*' and sys_platform != 'win32'", "python_full_version < '3.10' and sys_platform != 'win32'", ] @@ -351,6 +353,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/75/49e5bfe642f71f272236b5b2d2691cf915a7283cc0ceda56357b61daa538/comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3", size = 7180, upload-time = "2024-03-12T16:53:39.226Z" }, ] +[[package]] +name = "configargparse" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/4d/6c9ef746dfcc2a32e26f3860bb4a011c008c392b83eabdfb598d1a8bbe5d/configargparse-1.7.1.tar.gz", hash = "sha256:79c2ddae836a1e5914b71d58e4b9adbd9f7779d4e6351a637b7d2d9b6c46d3d9", size = 43958, upload-time = "2025-05-23T14:26:17.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/28/d28211d29bcc3620b1fece85a65ce5bb22f18670a03cd28ea4b75ede270c/configargparse-1.7.1-py3-none-any.whl", hash = "sha256:8b586a31f9d873abd1ca527ffbe58863c99f36d896e2829779803125e83be4b6", size = 25607, upload-time = "2025-05-23T14:26:15.923Z" }, +] + [[package]] name = "contourpy" version = "1.3.0" @@ -506,8 +517,10 @@ name = "contourpy" version = "1.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.11' and sys_platform == 'win32'", - "python_full_version >= '3.11' and sys_platform != 'win32'", + "python_full_version >= '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.13' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'win32'", ] dependencies = [ { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, @@ -730,6 +743,13 @@ wheels = [ name = "example-fgen-basic" version = "0.1.0a1" source = { editable = "." } +dependencies = [ + { name = "attrs" }, + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions" }, +] [package.optional-dependencies] full = [ @@ -743,666 +763,157 @@ plots = [ [package.dev-dependencies] all-dev = [ - { name = "anyio" }, - { name = "appnope", marker = "sys_platform == 'darwin'" }, - { name = "argon2-cffi" }, - { name = "argon2-cffi-bindings" }, - { name = "arrow" }, - { name = "asttokens" }, - { name = "async-lru" }, { name = "attrs" }, - { name = "babel" }, - { name = "backrefs" }, - { name = "beautifulsoup4" }, - { name = "bleach" }, - { name = "certifi" }, - { name = "cffi" }, - { name = "cfgv" }, - { name = "charset-normalizer" }, - { name = "click" }, - { name = "colorama" }, - { name = "comm" }, - { name = "coverage" }, - { name = "debugpy" }, - { name = "decorator" }, - { name = "defusedxml" }, - { name = "distlib" }, - { name = "executing" }, - { name = "fastjsonschema" }, - { name = "filelock" }, - { name = "fqdn" }, - { name = "ghp-import" }, - { name = "griffe" }, - { name = "h11" }, - { name = "httpcore" }, - { name = "httpx" }, - { name = "identify" }, - { name = "idna" }, - { name = "iniconfig" }, - { name = "ipykernel" }, - { name = "isoduration" }, - { name = "jedi" }, - { name = "jinja2" }, - { name = "json5" }, - { name = "jsonpointer" }, - { name = "jsonschema" }, - { name = "jsonschema-specifications" }, - { name = "jupyter-client" }, - { name = "jupyter-core" }, - { name = "jupyter-events" }, - { name = "jupyter-lsp" }, - { name = "jupyter-server" }, - { name = "jupyter-server-terminals" }, + { name = "fprettify" }, { name = "jupyterlab" }, - { name = "jupyterlab-pygments" }, - { name = "jupyterlab-server" }, { name = "jupytext" }, { name = "liccheck" }, - { name = "markdown" }, - { name = "markdown-it-py" }, - { name = "markupsafe" }, - { name = "matplotlib-inline" }, - { name = "mdit-py-plugins" }, - { name = "mdurl" }, - { name = "mergedeep" }, - { name = "mistune" }, { name = "mkdocs" }, { name = "mkdocs-autorefs" }, { name = "mkdocs-gen-files" }, - { name = "mkdocs-get-deps" }, { name = "mkdocs-jupyter" }, { name = "mkdocs-literate-nav" }, { name = "mkdocs-material" }, - { name = "mkdocs-material-extensions" }, { name = "mkdocs-section-index" }, - { name = "mkdocstrings" }, { name = "mkdocstrings-python" }, { name = "mkdocstrings-python-xref" }, { name = "mypy" }, - { name = "mypy-extensions" }, - { name = "nbclient" }, - { name = "nbconvert" }, - { name = "nbformat" }, - { name = "nest-asyncio" }, - { name = "nodeenv" }, - { name = "notebook-shim" }, - { name = "overrides" }, - { name = "packaging" }, - { name = "paginate" }, - { name = "pandocfilters" }, - { name = "parso" }, - { name = "pathspec" }, + { name = "pint" }, { name = "pip" }, - { name = "platformdirs" }, - { name = "pluggy" }, { name = "pre-commit" }, - { name = "prometheus-client" }, - { name = "prompt-toolkit" }, - { name = "psutil" }, - { name = "pure-eval" }, - { name = "pycparser" }, - { name = "pygments" }, { name = "pymdown-extensions" }, { name = "pytest" }, { name = "pytest-cov" }, - { name = "python-dateutil" }, - { name = "python-json-logger" }, - { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, - { name = "pywinpty", marker = "os_name == 'nt'" }, - { name = "pyyaml" }, - { name = "pyyaml-env-tag" }, - { name = "pyzmq" }, - { name = "referencing" }, - { name = "requests" }, - { name = "rfc3339-validator" }, - { name = "rfc3986-validator" }, - { name = "rich" }, - { name = "rpds-py" }, { name = "ruff" }, - { name = "semantic-version" }, - { name = "send2trash" }, { name = "setuptools" }, - { name = "shellingham" }, - { name = "six" }, - { name = "sniffio" }, - { name = "soupsieve" }, - { name = "stack-data" }, - { name = "terminado" }, - { name = "tinycss2" }, - { name = "toml" }, { name = "tomli" }, { name = "tomli-w" }, - { name = "tornado" }, { name = "towncrier" }, - { name = "traitlets" }, { name = "typer" }, - { name = "types-python-dateutil" }, - { name = "typing-extensions" }, - { name = "uri-template" }, - { name = "urllib3" }, - { name = "virtualenv" }, - { name = "watchdog" }, - { name = "wcwidth" }, - { name = "webcolors" }, - { name = "webencodings" }, - { name = "websocket-client" }, ] dev = [ - { name = "cfgv" }, - { name = "click" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "distlib" }, - { name = "filelock" }, - { name = "identify" }, - { name = "jinja2" }, + { name = "fprettify" }, { name = "liccheck" }, - { name = "markdown-it-py" }, - { name = "markupsafe" }, - { name = "mdurl" }, { name = "mypy" }, - { name = "mypy-extensions" }, - { name = "nodeenv" }, + { name = "pint" }, { name = "pip" }, - { name = "platformdirs" }, { name = "pre-commit" }, - { name = "pygments" }, - { name = "pyyaml" }, - { name = "rich" }, - { name = "semantic-version" }, { name = "setuptools" }, - { name = "shellingham" }, - { name = "toml" }, { name = "tomli" }, { name = "tomli-w" }, { name = "towncrier" }, { name = "typer" }, - { name = "typing-extensions" }, - { name = "virtualenv" }, ] docs = [ - { name = "anyio" }, - { name = "appnope", marker = "sys_platform == 'darwin'" }, - { name = "argon2-cffi" }, - { name = "argon2-cffi-bindings" }, - { name = "arrow" }, - { name = "asttokens" }, - { name = "async-lru" }, { name = "attrs" }, - { name = "babel" }, - { name = "backrefs" }, - { name = "beautifulsoup4" }, - { name = "bleach" }, - { name = "certifi" }, - { name = "cffi" }, - { name = "charset-normalizer" }, - { name = "click" }, - { name = "colorama" }, - { name = "comm" }, - { name = "debugpy" }, - { name = "decorator" }, - { name = "defusedxml" }, - { name = "executing" }, - { name = "fastjsonschema" }, - { name = "fqdn" }, - { name = "ghp-import" }, - { name = "griffe" }, - { name = "h11" }, - { name = "httpcore" }, - { name = "httpx" }, - { name = "idna" }, - { name = "ipykernel" }, - { name = "isoduration" }, - { name = "jedi" }, - { name = "jinja2" }, - { name = "json5" }, - { name = "jsonpointer" }, - { name = "jsonschema" }, - { name = "jsonschema-specifications" }, - { name = "jupyter-client" }, - { name = "jupyter-core" }, - { name = "jupyter-events" }, - { name = "jupyter-lsp" }, - { name = "jupyter-server" }, - { name = "jupyter-server-terminals" }, { name = "jupyterlab" }, - { name = "jupyterlab-pygments" }, - { name = "jupyterlab-server" }, { name = "jupytext" }, - { name = "markdown" }, - { name = "markdown-it-py" }, - { name = "markupsafe" }, - { name = "matplotlib-inline" }, - { name = "mdit-py-plugins" }, - { name = "mdurl" }, - { name = "mergedeep" }, - { name = "mistune" }, { name = "mkdocs" }, { name = "mkdocs-autorefs" }, { name = "mkdocs-gen-files" }, - { name = "mkdocs-get-deps" }, { name = "mkdocs-jupyter" }, { name = "mkdocs-literate-nav" }, { name = "mkdocs-material" }, - { name = "mkdocs-material-extensions" }, { name = "mkdocs-section-index" }, - { name = "mkdocstrings" }, { name = "mkdocstrings-python" }, { name = "mkdocstrings-python-xref" }, - { name = "nbclient" }, - { name = "nbconvert" }, - { name = "nbformat" }, - { name = "nest-asyncio" }, - { name = "notebook-shim" }, - { name = "overrides" }, - { name = "packaging" }, - { name = "paginate" }, - { name = "pandocfilters" }, - { name = "parso" }, - { name = "pathspec" }, - { name = "platformdirs" }, - { name = "prometheus-client" }, - { name = "prompt-toolkit" }, - { name = "psutil" }, - { name = "pure-eval" }, - { name = "pycparser" }, - { name = "pygments" }, { name = "pymdown-extensions" }, - { name = "python-dateutil" }, - { name = "python-json-logger" }, - { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, - { name = "pywinpty", marker = "os_name == 'nt'" }, - { name = "pyyaml" }, - { name = "pyyaml-env-tag" }, - { name = "pyzmq" }, - { name = "referencing" }, - { name = "requests" }, - { name = "rfc3339-validator" }, - { name = "rfc3986-validator" }, - { name = "rpds-py" }, { name = "ruff" }, - { name = "send2trash" }, - { name = "setuptools" }, - { name = "six" }, - { name = "sniffio" }, - { name = "soupsieve" }, - { name = "stack-data" }, - { name = "terminado" }, - { name = "tinycss2" }, - { name = "tornado" }, - { name = "traitlets" }, - { name = "types-python-dateutil" }, - { name = "uri-template" }, - { name = "urllib3" }, - { name = "watchdog" }, - { name = "wcwidth" }, - { name = "webcolors" }, - { name = "webencodings" }, - { name = "websocket-client" }, ] tests = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "coverage" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, { name = "pytest" }, { name = "pytest-cov" }, ] tests-full = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "coverage" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pytest" }, { name = "pytest-cov" }, ] tests-min = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, { name = "pytest" }, ] [package.metadata] requires-dist = [ + { name = "attrs", specifier = ">=24.3.0" }, { name = "example-fgen-basic", extras = ["plots"], marker = "extra == 'full'" }, { name = "matplotlib", marker = "extra == 'plots'", specifier = ">=3.7.1" }, + { name = "numpy", marker = "python_full_version < '3.13'", specifier = ">=1.26.0" }, + { name = "numpy", marker = "python_full_version >= '3.13'", specifier = ">=2.1.0" }, + { name = "typing-extensions", specifier = ">=4.12.2" }, ] provides-extras = ["plots", "full"] [package.metadata.requires-dev] all-dev = [ - { name = "anyio", specifier = "==4.8.0" }, - { name = "appnope", marker = "sys_platform == 'darwin'", specifier = "==0.1.4" }, - { name = "argon2-cffi", specifier = "==23.1.0" }, - { name = "argon2-cffi-bindings", specifier = "==21.2.0" }, - { name = "arrow", specifier = "==1.3.0" }, - { name = "asttokens", specifier = "==3.0.0" }, - { name = "async-lru", specifier = "==2.0.4" }, - { name = "attrs", specifier = "==25.3.0" }, - { name = "babel", specifier = "==2.16.0" }, - { name = "backrefs", specifier = "==5.9" }, - { name = "beautifulsoup4", specifier = "==4.12.3" }, - { name = "bleach", specifier = "==6.2.0" }, - { name = "certifi", specifier = "==2024.12.14" }, - { name = "cffi", specifier = "==1.17.1" }, - { name = "cfgv", specifier = "==3.4.0" }, - { name = "charset-normalizer", specifier = "==3.4.1" }, - { name = "click", specifier = "==8.1.8" }, - { name = "colorama", specifier = "==0.4.6" }, - { name = "colorama", marker = "sys_platform == 'win32'", specifier = "==0.4.6" }, - { name = "comm", specifier = "==0.2.2" }, - { name = "coverage", specifier = "==7.6.10" }, - { name = "debugpy", specifier = "==1.8.11" }, - { name = "decorator", specifier = "==5.1.1" }, - { name = "defusedxml", specifier = "==0.7.1" }, - { name = "distlib", specifier = "==0.3.9" }, - { name = "executing", specifier = "==2.1.0" }, - { name = "fastjsonschema", specifier = "==2.21.1" }, - { name = "filelock", specifier = "==3.16.1" }, - { name = "fqdn", specifier = "==1.5.1" }, - { name = "ghp-import", specifier = "==2.1.0" }, - { name = "griffe", specifier = "==1.11.0" }, - { name = "h11", specifier = "==0.14.0" }, - { name = "httpcore", specifier = "==1.0.7" }, - { name = "httpx", specifier = "==0.28.1" }, - { name = "identify", specifier = "==2.6.5" }, - { name = "idna", specifier = "==3.10" }, - { name = "iniconfig", specifier = "==2.0.0" }, - { name = "ipykernel", specifier = "==6.29.5" }, - { name = "isoduration", specifier = "==20.11.0" }, - { name = "jedi", specifier = "==0.19.2" }, - { name = "jinja2", specifier = "==3.1.5" }, - { name = "json5", specifier = "==0.10.0" }, - { name = "jsonpointer", specifier = "==3.0.0" }, - { name = "jsonschema", specifier = "==4.23.0" }, - { name = "jsonschema-specifications", specifier = "==2024.10.1" }, - { name = "jupyter-client", specifier = "==8.6.3" }, - { name = "jupyter-core", specifier = "==5.7.2" }, - { name = "jupyter-events", specifier = "==0.11.0" }, - { name = "jupyter-lsp", specifier = "==2.2.5" }, - { name = "jupyter-server", specifier = "==2.15.0" }, - { name = "jupyter-server-terminals", specifier = "==0.5.3" }, - { name = "jupyterlab", specifier = "==4.4.5" }, - { name = "jupyterlab-pygments", specifier = "==0.3.0" }, - { name = "jupyterlab-server", specifier = "==2.27.3" }, - { name = "jupytext", specifier = "==1.17.2" }, - { name = "liccheck", specifier = "==0.9.2" }, - { name = "markdown", specifier = "==3.7" }, - { name = "markdown-it-py", specifier = "==3.0.0" }, - { name = "markupsafe", specifier = "==3.0.2" }, - { name = "matplotlib-inline", specifier = "==0.1.7" }, - { name = "mdit-py-plugins", specifier = "==0.4.2" }, - { name = "mdurl", specifier = "==0.1.2" }, - { name = "mergedeep", specifier = "==1.3.4" }, - { name = "mistune", specifier = "==3.0.2" }, - { name = "mkdocs", specifier = "==1.6.1" }, - { name = "mkdocs-autorefs", specifier = "==1.4.2" }, - { name = "mkdocs-gen-files", specifier = "==0.5.0" }, - { name = "mkdocs-get-deps", specifier = "==0.2.0" }, - { name = "mkdocs-jupyter", specifier = "==0.25.1" }, - { name = "mkdocs-literate-nav", specifier = "==0.6.2" }, - { name = "mkdocs-material", specifier = "==9.6.16" }, - { name = "mkdocs-material-extensions", specifier = "==1.3.1" }, - { name = "mkdocs-section-index", specifier = "==0.3.10" }, - { name = "mkdocstrings", specifier = "==0.30.0" }, - { name = "mkdocstrings-python", specifier = "==1.16.12" }, - { name = "mkdocstrings-python-xref", specifier = "==1.16.3" }, - { name = "mypy", specifier = "==1.14.0" }, - { name = "mypy-extensions", specifier = "==1.0.0" }, - { name = "nbclient", specifier = "==0.10.2" }, - { name = "nbconvert", specifier = "==7.16.4" }, - { name = "nbformat", specifier = "==5.10.4" }, - { name = "nest-asyncio", specifier = "==1.6.0" }, - { name = "nodeenv", specifier = "==1.9.1" }, - { name = "notebook-shim", specifier = "==0.2.4" }, - { name = "overrides", specifier = "==7.7.0" }, - { name = "packaging", specifier = "==24.2" }, - { name = "paginate", specifier = "==0.5.7" }, - { name = "pandocfilters", specifier = "==1.5.1" }, - { name = "parso", specifier = "==0.8.4" }, - { name = "pathspec", specifier = "==0.12.1" }, - { name = "pip", specifier = "==24.3.1" }, - { name = "platformdirs", specifier = "==4.3.6" }, - { name = "pluggy", specifier = "==1.5.0" }, - { name = "pre-commit", specifier = "==4.0.1" }, - { name = "prometheus-client", specifier = "==0.21.1" }, - { name = "prompt-toolkit", specifier = "==3.0.48" }, - { name = "psutil", specifier = "==6.1.1" }, - { name = "pure-eval", specifier = "==0.2.3" }, - { name = "pycparser", specifier = "==2.22" }, - { name = "pygments", specifier = "==2.19.1" }, - { name = "pymdown-extensions", specifier = "==10.16.1" }, - { name = "pytest", specifier = "==8.3.4" }, - { name = "pytest-cov", specifier = "==6.0.0" }, - { name = "python-dateutil", specifier = "==2.9.0.post0" }, - { name = "python-json-logger", specifier = "==3.2.1" }, - { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'", specifier = "==308" }, - { name = "pywinpty", marker = "os_name == 'nt'", specifier = "==2.0.14" }, - { name = "pyyaml", specifier = "==6.0.2" }, - { name = "pyyaml-env-tag", specifier = "==0.1" }, - { name = "pyzmq", specifier = "==26.2.0" }, - { name = "referencing", specifier = "==0.35.1" }, - { name = "requests", specifier = "==2.32.3" }, - { name = "rfc3339-validator", specifier = "==0.1.4" }, - { name = "rfc3986-validator", specifier = "==0.1.1" }, - { name = "rich", specifier = "==13.9.4" }, - { name = "rpds-py", specifier = "==0.22.3" }, - { name = "ruff", specifier = "==0.12.8" }, - { name = "semantic-version", specifier = "==2.10.0" }, - { name = "send2trash", specifier = "==1.8.3" }, - { name = "setuptools", specifier = "==75.6.0" }, - { name = "shellingham", specifier = "==1.5.4" }, - { name = "six", specifier = "==1.17.0" }, - { name = "sniffio", specifier = "==1.3.1" }, - { name = "soupsieve", specifier = "==2.6" }, - { name = "stack-data", specifier = "==0.6.3" }, - { name = "terminado", specifier = "==0.18.1" }, - { name = "tinycss2", specifier = "==1.4.0" }, - { name = "toml", specifier = "==0.10.2" }, - { name = "tomli", specifier = "==2.2.1" }, - { name = "tomli-w", specifier = "==1.2.0" }, - { name = "tornado", specifier = "==6.4.2" }, - { name = "towncrier", specifier = "==24.8.0" }, - { name = "traitlets", specifier = "==5.14.3" }, - { name = "typer", specifier = "==0.15.2" }, - { name = "types-python-dateutil", specifier = "==2.9.0.20241206" }, - { name = "typing-extensions", specifier = "==4.12.2" }, - { name = "uri-template", specifier = "==1.3.0" }, - { name = "urllib3", specifier = "==2.3.0" }, - { name = "virtualenv", specifier = "==20.28.1" }, - { name = "watchdog", specifier = "==6.0.0" }, - { name = "wcwidth", specifier = "==0.2.13" }, - { name = "webcolors", specifier = "==24.11.1" }, - { name = "webencodings", specifier = "==0.5.1" }, - { name = "websocket-client", specifier = "==1.8.0" }, + { name = "attrs", specifier = ">=25.3.0" }, + { name = "fprettify", specifier = ">=0.3.7" }, + { name = "jupyterlab", specifier = ">=4.4.5" }, + { name = "jupytext", specifier = ">=1.17.2" }, + { name = "liccheck", specifier = ">=0.9.2" }, + { name = "mkdocs", specifier = ">=1.6.1" }, + { name = "mkdocs-autorefs", specifier = ">=1.4.2" }, + { name = "mkdocs-gen-files", specifier = ">=0.5.0" }, + { name = "mkdocs-jupyter", specifier = ">=0.25.1" }, + { name = "mkdocs-literate-nav", specifier = ">=0.6.2" }, + { name = "mkdocs-material", specifier = ">=9.6.16" }, + { name = "mkdocs-section-index", specifier = ">=0.3.10" }, + { name = "mkdocstrings-python", specifier = ">=1.16.12" }, + { name = "mkdocstrings-python-xref", specifier = ">=1.16.3" }, + { name = "mypy", specifier = ">=1.14.0" }, + { name = "pint", specifier = ">=0.24.4" }, + { name = "pip", specifier = ">=24.3.1" }, + { name = "pre-commit", specifier = ">=4.0.1" }, + { name = "pymdown-extensions", specifier = ">=10.16.1" }, + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, + { name = "ruff", specifier = ">=0.12.8" }, + { name = "setuptools", specifier = ">=75.6.0" }, + { name = "tomli", specifier = ">=2.2.1" }, + { name = "tomli-w", specifier = ">=1.2.0" }, + { name = "towncrier", specifier = ">=24.8.0" }, + { name = "typer", specifier = ">=0.15.2" }, ] dev = [ - { name = "cfgv", specifier = "==3.4.0" }, - { name = "click", specifier = "==8.1.8" }, - { name = "colorama", marker = "sys_platform == 'win32'", specifier = "==0.4.6" }, - { name = "distlib", specifier = "==0.3.9" }, - { name = "filelock", specifier = "==3.16.1" }, - { name = "identify", specifier = "==2.6.5" }, - { name = "jinja2", specifier = "==3.1.5" }, - { name = "liccheck", specifier = "==0.9.2" }, - { name = "markdown-it-py", specifier = "==3.0.0" }, - { name = "markupsafe", specifier = "==3.0.2" }, - { name = "mdurl", specifier = "==0.1.2" }, - { name = "mypy", specifier = "==1.14.0" }, - { name = "mypy-extensions", specifier = "==1.0.0" }, - { name = "nodeenv", specifier = "==1.9.1" }, - { name = "pip", specifier = "==24.3.1" }, - { name = "platformdirs", specifier = "==4.3.6" }, - { name = "pre-commit", specifier = "==4.0.1" }, - { name = "pygments", specifier = "==2.19.1" }, - { name = "pyyaml", specifier = "==6.0.2" }, - { name = "rich", specifier = "==13.9.4" }, - { name = "semantic-version", specifier = "==2.10.0" }, - { name = "setuptools", specifier = "==75.6.0" }, - { name = "shellingham", specifier = "==1.5.4" }, - { name = "toml", specifier = "==0.10.2" }, - { name = "tomli", specifier = "==2.2.1" }, - { name = "tomli-w", specifier = "==1.2.0" }, - { name = "towncrier", specifier = "==24.8.0" }, - { name = "typer", specifier = "==0.15.2" }, - { name = "typing-extensions", specifier = "==4.12.2" }, - { name = "virtualenv", specifier = "==20.28.1" }, + { name = "fprettify", specifier = ">=0.3.7" }, + { name = "liccheck", specifier = ">=0.9.2" }, + { name = "mypy", specifier = ">=1.14.0" }, + { name = "pint", specifier = ">=0.24.4" }, + { name = "pip", specifier = ">=24.3.1" }, + { name = "pre-commit", specifier = ">=4.0.1" }, + { name = "setuptools", specifier = ">=75.6.0" }, + { name = "tomli", specifier = ">=2.2.1" }, + { name = "tomli-w", specifier = ">=1.2.0" }, + { name = "towncrier", specifier = ">=24.8.0" }, + { name = "typer", specifier = ">=0.15.2" }, ] docs = [ - { name = "anyio", specifier = "==4.8.0" }, - { name = "appnope", marker = "sys_platform == 'darwin'", specifier = "==0.1.4" }, - { name = "argon2-cffi", specifier = "==23.1.0" }, - { name = "argon2-cffi-bindings", specifier = "==21.2.0" }, - { name = "arrow", specifier = "==1.3.0" }, - { name = "asttokens", specifier = "==3.0.0" }, - { name = "async-lru", specifier = "==2.0.4" }, - { name = "attrs", specifier = "==25.3.0" }, - { name = "babel", specifier = "==2.16.0" }, - { name = "backrefs", specifier = "==5.9" }, - { name = "beautifulsoup4", specifier = "==4.12.3" }, - { name = "bleach", specifier = "==6.2.0" }, - { name = "certifi", specifier = "==2024.12.14" }, - { name = "cffi", specifier = "==1.17.1" }, - { name = "charset-normalizer", specifier = "==3.4.1" }, - { name = "click", specifier = "==8.1.8" }, - { name = "colorama", specifier = "==0.4.6" }, - { name = "comm", specifier = "==0.2.2" }, - { name = "debugpy", specifier = "==1.8.11" }, - { name = "decorator", specifier = "==5.1.1" }, - { name = "defusedxml", specifier = "==0.7.1" }, - { name = "executing", specifier = "==2.1.0" }, - { name = "fastjsonschema", specifier = "==2.21.1" }, - { name = "fqdn", specifier = "==1.5.1" }, - { name = "ghp-import", specifier = "==2.1.0" }, - { name = "griffe", specifier = "==1.11.0" }, - { name = "h11", specifier = "==0.14.0" }, - { name = "httpcore", specifier = "==1.0.7" }, - { name = "httpx", specifier = "==0.28.1" }, - { name = "idna", specifier = "==3.10" }, - { name = "ipykernel", specifier = "==6.29.5" }, - { name = "isoduration", specifier = "==20.11.0" }, - { name = "jedi", specifier = "==0.19.2" }, - { name = "jinja2", specifier = "==3.1.5" }, - { name = "json5", specifier = "==0.10.0" }, - { name = "jsonpointer", specifier = "==3.0.0" }, - { name = "jsonschema", specifier = "==4.23.0" }, - { name = "jsonschema-specifications", specifier = "==2024.10.1" }, - { name = "jupyter-client", specifier = "==8.6.3" }, - { name = "jupyter-core", specifier = "==5.7.2" }, - { name = "jupyter-events", specifier = "==0.11.0" }, - { name = "jupyter-lsp", specifier = "==2.2.5" }, - { name = "jupyter-server", specifier = "==2.15.0" }, - { name = "jupyter-server-terminals", specifier = "==0.5.3" }, - { name = "jupyterlab", specifier = "==4.4.5" }, - { name = "jupyterlab-pygments", specifier = "==0.3.0" }, - { name = "jupyterlab-server", specifier = "==2.27.3" }, - { name = "jupytext", specifier = "==1.17.2" }, - { name = "markdown", specifier = "==3.7" }, - { name = "markdown-it-py", specifier = "==3.0.0" }, - { name = "markupsafe", specifier = "==3.0.2" }, - { name = "matplotlib-inline", specifier = "==0.1.7" }, - { name = "mdit-py-plugins", specifier = "==0.4.2" }, - { name = "mdurl", specifier = "==0.1.2" }, - { name = "mergedeep", specifier = "==1.3.4" }, - { name = "mistune", specifier = "==3.0.2" }, - { name = "mkdocs", specifier = "==1.6.1" }, - { name = "mkdocs-autorefs", specifier = "==1.4.2" }, - { name = "mkdocs-gen-files", specifier = "==0.5.0" }, - { name = "mkdocs-get-deps", specifier = "==0.2.0" }, - { name = "mkdocs-jupyter", specifier = "==0.25.1" }, - { name = "mkdocs-literate-nav", specifier = "==0.6.2" }, - { name = "mkdocs-material", specifier = "==9.6.16" }, - { name = "mkdocs-material-extensions", specifier = "==1.3.1" }, - { name = "mkdocs-section-index", specifier = "==0.3.10" }, - { name = "mkdocstrings", specifier = "==0.30.0" }, - { name = "mkdocstrings-python", specifier = "==1.16.12" }, - { name = "mkdocstrings-python-xref", specifier = "==1.16.3" }, - { name = "nbclient", specifier = "==0.10.2" }, - { name = "nbconvert", specifier = "==7.16.4" }, - { name = "nbformat", specifier = "==5.10.4" }, - { name = "nest-asyncio", specifier = "==1.6.0" }, - { name = "notebook-shim", specifier = "==0.2.4" }, - { name = "overrides", specifier = "==7.7.0" }, - { name = "packaging", specifier = "==24.2" }, - { name = "paginate", specifier = "==0.5.7" }, - { name = "pandocfilters", specifier = "==1.5.1" }, - { name = "parso", specifier = "==0.8.4" }, - { name = "pathspec", specifier = "==0.12.1" }, - { name = "platformdirs", specifier = "==4.3.6" }, - { name = "prometheus-client", specifier = "==0.21.1" }, - { name = "prompt-toolkit", specifier = "==3.0.48" }, - { name = "psutil", specifier = "==6.1.1" }, - { name = "pure-eval", specifier = "==0.2.3" }, - { name = "pycparser", specifier = "==2.22" }, - { name = "pygments", specifier = "==2.19.1" }, - { name = "pymdown-extensions", specifier = "==10.16.1" }, - { name = "python-dateutil", specifier = "==2.9.0.post0" }, - { name = "python-json-logger", specifier = "==3.2.1" }, - { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'", specifier = "==308" }, - { name = "pywinpty", marker = "os_name == 'nt'", specifier = "==2.0.14" }, - { name = "pyyaml", specifier = "==6.0.2" }, - { name = "pyyaml-env-tag", specifier = "==0.1" }, - { name = "pyzmq", specifier = "==26.2.0" }, - { name = "referencing", specifier = "==0.35.1" }, - { name = "requests", specifier = "==2.32.3" }, - { name = "rfc3339-validator", specifier = "==0.1.4" }, - { name = "rfc3986-validator", specifier = "==0.1.1" }, - { name = "rpds-py", specifier = "==0.22.3" }, - { name = "ruff", specifier = "==0.12.8" }, - { name = "send2trash", specifier = "==1.8.3" }, - { name = "setuptools", specifier = "==75.6.0" }, - { name = "six", specifier = "==1.17.0" }, - { name = "sniffio", specifier = "==1.3.1" }, - { name = "soupsieve", specifier = "==2.6" }, - { name = "stack-data", specifier = "==0.6.3" }, - { name = "terminado", specifier = "==0.18.1" }, - { name = "tinycss2", specifier = "==1.4.0" }, - { name = "tornado", specifier = "==6.4.2" }, - { name = "traitlets", specifier = "==5.14.3" }, - { name = "types-python-dateutil", specifier = "==2.9.0.20241206" }, - { name = "uri-template", specifier = "==1.3.0" }, - { name = "urllib3", specifier = "==2.3.0" }, - { name = "watchdog", specifier = "==6.0.0" }, - { name = "wcwidth", specifier = "==0.2.13" }, - { name = "webcolors", specifier = "==24.11.1" }, - { name = "webencodings", specifier = "==0.5.1" }, - { name = "websocket-client", specifier = "==1.8.0" }, + { name = "attrs", specifier = ">=25.3.0" }, + { name = "jupyterlab", specifier = ">=4.4.5" }, + { name = "jupytext", specifier = ">=1.17.2" }, + { name = "mkdocs", specifier = ">=1.6.1" }, + { name = "mkdocs-autorefs", specifier = ">=1.4.2" }, + { name = "mkdocs-gen-files", specifier = ">=0.5.0" }, + { name = "mkdocs-jupyter", specifier = ">=0.25.1" }, + { name = "mkdocs-literate-nav", specifier = ">=0.6.2" }, + { name = "mkdocs-material", specifier = ">=9.6.16" }, + { name = "mkdocs-section-index", specifier = ">=0.3.10" }, + { name = "mkdocstrings-python", specifier = ">=1.16.12" }, + { name = "mkdocstrings-python-xref", specifier = ">=1.16.3" }, + { name = "pymdown-extensions", specifier = ">=10.16.1" }, + { name = "ruff", specifier = ">=0.12.8" }, ] tests = [ - { name = "colorama", marker = "sys_platform == 'win32'", specifier = "==0.4.6" }, - { name = "coverage", specifier = "==7.6.10" }, - { name = "iniconfig", specifier = "==2.0.0" }, - { name = "packaging", specifier = "==24.2" }, - { name = "pluggy", specifier = "==1.5.0" }, - { name = "pytest", specifier = "==8.3.4" }, - { name = "pytest-cov", specifier = "==6.0.0" }, -] -tests-full = [ - { name = "colorama", marker = "sys_platform == 'win32'", specifier = "==0.4.6" }, - { name = "coverage", specifier = "==7.6.10" }, - { name = "iniconfig", specifier = "==2.0.0" }, - { name = "packaging", specifier = "==24.2" }, - { name = "pluggy", specifier = "==1.5.0" }, - { name = "pytest", specifier = "==8.3.4" }, - { name = "pytest-cov", specifier = "==6.0.0" }, -] -tests-min = [ - { name = "colorama", marker = "sys_platform == 'win32'", specifier = "==0.4.6" }, - { name = "iniconfig", specifier = "==2.0.0" }, - { name = "packaging", specifier = "==24.2" }, - { name = "pluggy", specifier = "==1.5.0" }, - { name = "pytest", specifier = "==8.3.4" }, + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, ] +tests-full = [{ name = "pytest-cov", specifier = ">=6.0.0" }] +tests-min = [{ name = "pytest", specifier = ">=8.3.4" }] [[package]] name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ @@ -1436,6 +947,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163, upload-time = "2024-09-17T19:02:00.268Z" }, ] +[[package]] +name = "flexcache" +version = "0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/b0/8a21e330561c65653d010ef112bf38f60890051d244ede197ddaa08e50c1/flexcache-0.3.tar.gz", hash = "sha256:18743bd5a0621bfe2cf8d519e4c3bfdf57a269c15d1ced3fb4b64e0ff4600656", size = 15816, upload-time = "2024-03-09T03:21:07.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/cd/c883e1a7c447479d6e13985565080e3fea88ab5a107c21684c813dba1875/flexcache-0.3-py3-none-any.whl", hash = "sha256:d43c9fea82336af6e0115e308d9d33a185390b8346a017564611f1466dcd2e32", size = 13263, upload-time = "2024-03-09T03:21:05.635Z" }, +] + +[[package]] +name = "flexparser" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/99/b4de7e39e8eaf8207ba1a8fa2241dd98b2ba72ae6e16960d8351736d8702/flexparser-0.4.tar.gz", hash = "sha256:266d98905595be2ccc5da964fe0a2c3526fbbffdc45b65b3146d75db992ef6b2", size = 31799, upload-time = "2024-11-07T02:00:56.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl", hash = "sha256:3738b456192dcb3e15620f324c447721023c0293f6af9955b481e91d00179846", size = 27625, upload-time = "2024-11-07T02:00:54.523Z" }, +] + [[package]] name = "fonttools" version = "4.59.0" @@ -1485,6 +1020,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/9c/df0ef2c51845a13043e5088f7bb988ca6cd5bb82d5d4203d6a158aa58cf2/fonttools-4.59.0-py3-none-any.whl", hash = "sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d", size = 1128050, upload-time = "2025-07-16T12:04:52.687Z" }, ] +[[package]] +name = "fprettify" +version = "0.3.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "configargparse" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/15/d88681bd2be4a375a78b52443b8e87608240913623d9be5c47e3c328b068/fprettify-0.3.7.tar.gz", hash = "sha256:1488a813f7e60a9e86c56fd0b82bd9df1b75bfb4bf2ee8e433c12f63b7e54057", size = 29639, upload-time = "2020-11-20T15:52:49.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/13/2c32d63574e116f8c933f56315df9135bf2fae7a88e9e7c6c4d37f48f4ef/fprettify-0.3.7-py3-none-any.whl", hash = "sha256:56f0a64c43dc47134ce32af2e5da8cd7a1584897be29d19289ec5d87510d1daf", size = 28095, upload-time = "2020-11-20T15:52:47.719Z" }, +] + [[package]] name = "fqdn" version = "1.5.1" @@ -1578,7 +1125,7 @@ name = "importlib-metadata" version = "8.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp" }, + { name = "zipp", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } wheels = [ @@ -1689,8 +1236,10 @@ name = "ipython" version = "9.4.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.11' and sys_platform == 'win32'", - "python_full_version >= '3.11' and sys_platform != 'win32'", + "python_full_version >= '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.13' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'win32'", ] dependencies = [ { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, @@ -2100,9 +1649,11 @@ name = "kiwisolver" version = "1.4.9" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.11' and sys_platform == 'win32'", + "python_full_version >= '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", "python_full_version == '3.10.*' and sys_platform == 'win32'", - "python_full_version >= '3.11' and sys_platform != 'win32'", + "python_full_version >= '3.13' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'win32'", "python_full_version == '3.10.*' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } @@ -2383,9 +1934,11 @@ name = "matplotlib" version = "3.10.5" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.11' and sys_platform == 'win32'", + "python_full_version >= '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", "python_full_version == '3.10.*' and sys_platform == 'win32'", - "python_full_version >= '3.11' and sys_platform != 'win32'", + "python_full_version >= '3.13' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'win32'", "python_full_version == '3.10.*' and sys_platform != 'win32'", ] dependencies = [ @@ -2956,8 +2509,10 @@ name = "numpy" version = "2.3.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.11' and sys_platform == 'win32'", - "python_full_version >= '3.11' and sys_platform != 'win32'", + "python_full_version >= '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.13' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" } wheels = [ @@ -3215,6 +2770,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, ] +[[package]] +name = "pint" +version = "0.24.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flexcache" }, + { name = "flexparser" }, + { name = "platformdirs" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/bb/52b15ddf7b7706ed591134a895dbf6e41c8348171fb635e655e0a4bbb0ea/pint-0.24.4.tar.gz", hash = "sha256:35275439b574837a6cd3020a5a4a73645eb125ce4152a73a2f126bf164b91b80", size = 342225, upload-time = "2024-11-07T16:29:46.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/16/bd2f5904557265882108dc2e04f18abc05ab0c2b7082ae9430091daf1d5c/Pint-0.24.4-py3-none-any.whl", hash = "sha256:aa54926c8772159fcf65f82cc0d34de6768c151b32ad1deb0331291c38fe7659", size = 302029, upload-time = "2024-11-07T16:29:43.976Z" }, +] + [[package]] name = "pip" version = "24.3.1"