From fa73241e49f9014ce511f3de36c89e54ea8f41e5 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 09:59:50 +0200 Subject: [PATCH 01/32] Add Fortran build stuff --- .fprettify.rc | 2 + .gitignore | 19 +- Makefile | 27 ++ config/install-mod.py | 50 +++ config/meson.build | 25 ++ meson.build | 228 +++++++++++ meson_options.txt | 28 ++ pyproject.toml | 1 + src/example_fgen_basic/__init__.py | 6 +- src/example_fgen_basic/exceptions.py | 81 ++++ src/example_fgen_basic/get_wavelength.f90 | 36 ++ src/example_fgen_basic/get_wavelength.py | 69 ++++ .../get_wavelength_wrapper.f90 | 41 ++ src/example_fgen_basic/kind_parameters.f90 | 24 ++ src/example_fgen_basic/meson.build | 4 + src/example_fgen_basic/runtime_helpers.py | 383 ++++++++++++++++++ src/meson.build | 1 + subprojects/test-drive.wrap | 5 + tests/meson.build | 2 + tests/unit/main.f90 | 54 +++ tests/unit/meson.build | 31 ++ tests/unit/test_get_wavelength.f90 | 48 +++ tests/unit/test_get_wavelength.py | 21 + uv.lock | 29 +- 24 files changed, 1205 insertions(+), 10 deletions(-) create mode 100644 .fprettify.rc create mode 100644 config/install-mod.py create mode 100644 config/meson.build create mode 100644 meson.build create mode 100644 meson_options.txt create mode 100644 src/example_fgen_basic/exceptions.py create mode 100644 src/example_fgen_basic/get_wavelength.f90 create mode 100644 src/example_fgen_basic/get_wavelength.py create mode 100644 src/example_fgen_basic/get_wavelength_wrapper.f90 create mode 100644 src/example_fgen_basic/kind_parameters.f90 create mode 100644 src/example_fgen_basic/meson.build create mode 100644 src/example_fgen_basic/runtime_helpers.py create mode 100644 src/meson.build create mode 100644 subprojects/test-drive.wrap create mode 100644 tests/meson.build create mode 100644 tests/unit/main.f90 create mode 100644 tests/unit/meson.build create mode 100644 tests/unit/test_get_wavelength.f90 create mode 100644 tests/unit/test_get_wavelength.py 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/.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/Makefile b/Makefile index 5382d7b..501d188 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,8 @@ # 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 # A helper script to get short descriptions of each target in the Makefile define PRINT_HELP_PYSCRIPT @@ -81,3 +83,28 @@ licence-check: ## Check that licences of the dependencies are suitable 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 + +.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/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/meson.build b/meson.build new file mode 100644 index 0000000..73bc3fd --- /dev/null +++ b/meson.build @@ -0,0 +1,228 @@ +project( + 'example', # 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' # 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/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/get_wavelength.f90', + 'src/example/kind_parameters.f90', + ) + + # All Python files (wrappers and otherwise) + # Injected with `script/inject-srcs-into-meson-build.py` + python_srcs = files( + 'src/example/__init__.py', + 'src/example/exceptions.py', + 'src/example/get_wavelength.py', + 'src/example/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: 'Example of wrapping Fortran so it can be called from Python. 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..f4d35c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,6 +90,7 @@ dev = [ "pygments==2.19.1", "rich==13.9.4", "shellingham==1.5.4", + "fprettify>=0.3.7", ] docs = [ # Key dependencies diff --git a/src/example_fgen_basic/__init__.py b/src/example_fgen_basic/__init__.py index 2ce9e8d..29c6141 100644 --- a/src/example_fgen_basic/__init__.py +++ b/src/example_fgen_basic/__init__.py @@ -1,7 +1,3 @@ """ -Basic example of using fgen +Example of wrapping Fortran so it can be accessed via Python """ - -import importlib.metadata - -__version__ = importlib.metadata.version("example_fgen_basic") 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..2c08a2c --- /dev/null +++ b/src/example_fgen_basic/get_wavelength.py @@ -0,0 +1,69 @@ +""" +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.exceptions import ( + CompiledExtensionNotFoundError, +) + +if TYPE_CHECKING: + import pint + +try: + from example._lib import m_get_wavelength_w # type: ignore +except (ModuleNotFoundError, ImportError) as exc: + raise CompiledExtensionNotFoundError("example._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 = 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..4cf9f09 --- /dev/null +++ b/src/example_fgen_basic/runtime_helpers.py @@ -0,0 +1,383 @@ +""" +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, Concatenate, ParamSpec, TypeVar + +import attrs +from attrs import define, field +from example.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..227dabf --- /dev/null +++ b/tests/unit/test_get_wavelength.py @@ -0,0 +1,21 @@ +""" +Dummy tests +""" + +import numpy as np +import pint +import pint.testing +from example.get_wavelength import get_wavelength, get_wavelength_plain + + +def test_plain(): + np.testing.assert_allclose(get_wavelength_plain(400.0e12), 12) + + +def test_units(): + 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..dec20f1 100644 --- a/uv.lock +++ b/uv.lock @@ -351,6 +351,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" @@ -770,6 +779,7 @@ all-dev = [ { name = "executing" }, { name = "fastjsonschema" }, { name = "filelock" }, + { name = "fprettify" }, { name = "fqdn" }, { name = "ghp-import" }, { name = "griffe" }, @@ -893,6 +903,7 @@ dev = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "distlib" }, { name = "filelock" }, + { name = "fprettify" }, { name = "identify" }, { name = "jinja2" }, { name = "liccheck" }, @@ -1102,6 +1113,7 @@ all-dev = [ { name = "executing", specifier = "==2.1.0" }, { name = "fastjsonschema", specifier = "==2.21.1" }, { name = "filelock", specifier = "==3.16.1" }, + { name = "fprettify", specifier = ">=0.3.7" }, { name = "fqdn", specifier = "==1.5.1" }, { name = "ghp-import", specifier = "==2.1.0" }, { name = "griffe", specifier = "==1.11.0" }, @@ -1225,6 +1237,7 @@ dev = [ { name = "colorama", marker = "sys_platform == 'win32'", specifier = "==0.4.6" }, { name = "distlib", specifier = "==0.3.9" }, { name = "filelock", specifier = "==3.16.1" }, + { name = "fprettify", specifier = ">=0.3.7" }, { name = "identify", specifier = "==2.6.5" }, { name = "jinja2", specifier = "==3.1.5" }, { name = "liccheck", specifier = "==0.9.2" }, @@ -1402,7 +1415,7 @@ 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 = [ @@ -1485,6 +1498,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 +1603,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 = [ From 4d4de649fd1b7114a97a796ed09f43a53dd88315 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 10:03:45 +0200 Subject: [PATCH 02/32] Up to installing using meson-python The command that should work is `make test`. In the meantime, you might need to use `uv run --no-editable --reinstall-package example-fgen-basic pytest tests` --- pyproject.toml | 1 + src/example_fgen_basic/get_wavelength.py | 8 +++----- src/example_fgen_basic/runtime_helpers.py | 3 ++- tests/unit/test_get_wavelength.py | 3 ++- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f4d35c5..4125f6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -260,6 +260,7 @@ 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" diff --git a/src/example_fgen_basic/get_wavelength.py b/src/example_fgen_basic/get_wavelength.py index 2c08a2c..80e0042 100644 --- a/src/example_fgen_basic/get_wavelength.py +++ b/src/example_fgen_basic/get_wavelength.py @@ -10,17 +10,15 @@ from typing import TYPE_CHECKING -from example.exceptions import ( - CompiledExtensionNotFoundError, -) +from example_fgen_basic.exceptions import CompiledExtensionNotFoundError if TYPE_CHECKING: import pint try: - from example._lib import m_get_wavelength_w # type: ignore + from example_fgen_basic._lib import m_get_wavelength_w # type: ignore except (ModuleNotFoundError, ImportError) as exc: - raise CompiledExtensionNotFoundError("example._lib") from exc + raise CompiledExtensionNotFoundError("example_fgen_basic._lib") from exc def get_wavelength_plain(frequency: float) -> float: diff --git a/src/example_fgen_basic/runtime_helpers.py b/src/example_fgen_basic/runtime_helpers.py index 4cf9f09..abe32d7 100644 --- a/src/example_fgen_basic/runtime_helpers.py +++ b/src/example_fgen_basic/runtime_helpers.py @@ -13,7 +13,8 @@ import attrs from attrs import define, field -from example.exceptions import NotInitialisedError, UnallocatedMemoryError + +from example_fgen_basic.exceptions import NotInitialisedError, UnallocatedMemoryError # Might be needed for Python 3.9 # from typing_extensions import Concatenate, ParamSpec diff --git a/tests/unit/test_get_wavelength.py b/tests/unit/test_get_wavelength.py index 227dabf..3176fee 100644 --- a/tests/unit/test_get_wavelength.py +++ b/tests/unit/test_get_wavelength.py @@ -5,7 +5,8 @@ import numpy as np import pint import pint.testing -from example.get_wavelength import get_wavelength, get_wavelength_plain + +from example_fgen_basic.get_wavelength import get_wavelength, get_wavelength_plain def test_plain(): From 4f716ca9931b8a4dd20f5190cf1a413041aabbda Mon Sep 17 00:00:00 2001 From: Marco Zecchetto Date: Thu, 14 Aug 2025 14:09:23 +0200 Subject: [PATCH 03/32] Added missing files and adjusted meson.build --- meson.build | 15 +++--- pyproject.toml | 19 ++++--- scratch.py | 18 +++++++ scripts/inject-srcs-into-meson-build.py | 67 +++++++++++++++++++++++++ scripts/propogate-pyproject-metadata.py | 50 ++++++++++++++++++ 5 files changed, 156 insertions(+), 13 deletions(-) create mode 100644 scratch.py create mode 100644 scripts/inject-srcs-into-meson-build.py create mode 100644 scripts/propogate-pyproject-metadata.py diff --git a/meson.build b/meson.build index 73bc3fd..6345e71 100644 --- a/meson.build +++ b/meson.build @@ -48,23 +48,24 @@ if pyprojectwheelbuild_enabled # These are the src with which Python interacts. # Injected with `script/inject-srcs-into-meson-build.py` srcs = files( - 'src/example/get_wavelength_wrapper.f90', + '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/get_wavelength.f90', - 'src/example/kind_parameters.f90', + '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/__init__.py', - 'src/example/exceptions.py', - 'src/example/get_wavelength.py', - 'src/example/runtime_helpers.py', + '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, diff --git a/pyproject.toml b/pyproject.toml index 4125f6f..fc43944 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -262,13 +262,20 @@ all-dev = [ # 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] diff --git a/scratch.py b/scratch.py new file mode 100644 index 0000000..881cc7d --- /dev/null +++ b/scratch.py @@ -0,0 +1,18 @@ +""" +Run with `uv run --no-editable --reinstall-package example python scratch.py` + +Requires no editable to avoid Fortran fun headaches +(although there are better ways to do this, +TODO: read https://mesonbuild.com/meson-python/how-to-guides/editable-installs.html). +Requires reinstall to pick up changes to src. +""" + +import pint + +from example.get_wavelength import get_wavelength, get_wavelength_plain + +ur = pint.get_application_registry() + +print(f"{get_wavelength_plain(400.0e12)=}") + +print(f"{get_wavelength(ur.Quantity(400.0, 'THz')).to('nm')=}") diff --git a/scripts/inject-srcs-into-meson-build.py b/scripts/inject-srcs-into-meson-build.py new file mode 100644 index 0000000..40e2a80 --- /dev/null +++ b/scripts/inject-srcs-into-meson-build.py @@ -0,0 +1,67 @@ +""" +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: + return f"{meson_variable} = files\(\s*('[a-z\/_\.0-9]*',\s*)*\)" + + +def get_src_substitution( + meson_variable: str, srcs: Iterable[Path], rel_to: Path +) -> str: + 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(): + REPO_ROOT = Path(__file__).parents[1] + SRC_DIR = REPO_ROOT / "src" + print(f"Source={SRC_DIR}") + 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..2ca3046 --- /dev/null +++ b/scripts/propogate-pyproject-metadata.py @@ -0,0 +1,50 @@ +""" +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(): + 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"] + 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 ( + ("version\: '[0-9a-z\.]*'", f"version: '{version}'"), + ( + "python_project_name = '[a-z\-_]*'", + f"python_project_name = '{project_name}'", + ), + ("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() From c981ffcdd1c42578ec19dc6c4a7abd2ac541b12d Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 14:21:49 +0200 Subject: [PATCH 04/32] Clean up a bit and check tests --- meson.build | 6 ++-- scratch.py | 18 ----------- scripts/inject-srcs-into-meson-build.py | 43 +++++++++++++++++++++++-- scripts/propogate-pyproject-metadata.py | 12 ++++--- tests/unit/test_get_wavelength.py | 10 +++--- 5 files changed, 57 insertions(+), 32 deletions(-) delete mode 100644 scratch.py diff --git a/meson.build b/meson.build index 6345e71..c7f8970 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project( - 'example', # injected with scripts/make-version-consistent.py, don't edit by hand + '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` @@ -28,7 +28,7 @@ endif if pyprojectwheelbuild_enabled ## Python wrapper - python_project_name = 'example' # injected with scripts/make-version-consistent.py, don't edit by hand + 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 @@ -213,7 +213,7 @@ else pkg = import('pkgconfig') pkg.generate( example_lib, - description: 'Example of wrapping Fortran so it can be called from Python. This is the standalone Fortran library.', # injected with scripts/make-version-consistent.py, do not edit by hand + 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], ) diff --git a/scratch.py b/scratch.py deleted file mode 100644 index 881cc7d..0000000 --- a/scratch.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -Run with `uv run --no-editable --reinstall-package example python scratch.py` - -Requires no editable to avoid Fortran fun headaches -(although there are better ways to do this, -TODO: read https://mesonbuild.com/meson-python/how-to-guides/editable-installs.html). -Requires reinstall to pick up changes to src. -""" - -import pint - -from example.get_wavelength import get_wavelength, get_wavelength_plain - -ur = pint.get_application_registry() - -print(f"{get_wavelength_plain(400.0e12)=}") - -print(f"{get_wavelength(ur.Quantity(400.0, 'THz')).to('nm')=}") diff --git a/scripts/inject-srcs-into-meson-build.py b/scripts/inject-srcs-into-meson-build.py index 40e2a80..3487b16 100644 --- a/scripts/inject-srcs-into-meson-build.py +++ b/scripts/inject-srcs-into-meson-build.py @@ -13,14 +13,46 @@ def get_src_pattern(meson_variable: str) -> str: - return f"{meson_variable} = files\(\s*('[a-z\/_\.0-9]*',\s*)*\)" + """ + 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)), + ",\n".join(f"'{v.relative_to(rel_to).as_posix()}'" for v in srcs), prefix=" " * 4, ) @@ -29,9 +61,14 @@ def get_src_substitution( def main(): + """ + Inject sources into `meson.build` + + Nicer than typing by hand + """ REPO_ROOT = Path(__file__).parents[1] SRC_DIR = REPO_ROOT / "src" - print(f"Source={SRC_DIR}") + srcs = [] srcs_ancillary_lib = [] for ffile in SRC_DIR.rglob("*.f90"): diff --git a/scripts/propogate-pyproject-metadata.py b/scripts/propogate-pyproject-metadata.py index 2ca3046..847c37f 100644 --- a/scripts/propogate-pyproject-metadata.py +++ b/scripts/propogate-pyproject-metadata.py @@ -11,11 +11,15 @@ 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 @@ -28,12 +32,12 @@ def main(): meson_build_out = meson_build_in for pattern, substitution in ( - ("version\: '[0-9a-z\.]*'", f"version: '{version}'"), + (r"version\: '[0-9a-z\.]*'", f"version: '{version}'"), ( - "python_project_name = '[a-z\-_]*'", - f"python_project_name = '{project_name}'", + r"python_project_name = '[a-z\-_]*'", + f"python_project_name = '{project_name_python}'", ), - ("project\(\s*'[a-z\-_]*'", f"project(\n '{project_name}'"), + (r"project\(\s*'[a-z\-_]*'", f"project(\n '{project_name}'"), ( "description: '.*'", f"description: '{description}. This is the standalone Fortran library.'", diff --git a/tests/unit/test_get_wavelength.py b/tests/unit/test_get_wavelength.py index 3176fee..c75ac87 100644 --- a/tests/unit/test_get_wavelength.py +++ b/tests/unit/test_get_wavelength.py @@ -3,20 +3,22 @@ """ import numpy as np -import pint -import pint.testing +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), 12) + 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( + pint_testing.assert_allclose( get_wavelength(ur.Quantity(400.0, "THz")).to("nm"), ur.Quantity(749.48, "nm"), ) From bd5954eb5ba6e32f109fd0fa0f51e078373800a5 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 14:26:33 +0200 Subject: [PATCH 05/32] Add pre-commit config for local stuff --- .pre-commit-config.yaml | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) 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 From 2675d2df7032fffdd55a38ac38d93bf312e0b617 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 14:28:10 +0200 Subject: [PATCH 06/32] Update make test command --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 501d188..f6b6513 100644 --- a/Makefile +++ b/Makefile @@ -39,8 +39,8 @@ 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) + uv run --no-editable --reinstall-package example-fgen-basic pytest tests src tests -r a -v --doctest-modules --doctest-report ndiff --cov=src # Note on code coverage and testing: # You must specify cov=src. From b4d6122b6e462dcc57e7326a4a16335098efa83d Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 14:31:41 +0200 Subject: [PATCH 07/32] Add pint to dev environment --- pyproject.toml | 1 + uv.lock | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index fc43944..9f8a267 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,6 +91,7 @@ dev = [ "rich==13.9.4", "shellingham==1.5.4", "fprettify>=0.3.7", + "pint>=0.24.4", ] docs = [ # Key dependencies diff --git a/uv.lock b/uv.lock index dec20f1..7c7fe71 100644 --- a/uv.lock +++ b/uv.lock @@ -842,6 +842,7 @@ all-dev = [ { name = "pandocfilters" }, { name = "parso" }, { name = "pathspec" }, + { name = "pint" }, { name = "pip" }, { name = "platformdirs" }, { name = "pluggy" }, @@ -913,6 +914,7 @@ dev = [ { name = "mypy" }, { name = "mypy-extensions" }, { name = "nodeenv" }, + { name = "pint" }, { name = "pip" }, { name = "platformdirs" }, { name = "pre-commit" }, @@ -1176,6 +1178,7 @@ all-dev = [ { name = "pandocfilters", specifier = "==1.5.1" }, { name = "parso", specifier = "==0.8.4" }, { name = "pathspec", specifier = "==0.12.1" }, + { name = "pint", specifier = ">=0.24.4" }, { name = "pip", specifier = "==24.3.1" }, { name = "platformdirs", specifier = "==4.3.6" }, { name = "pluggy", specifier = "==1.5.0" }, @@ -1247,6 +1250,7 @@ dev = [ { name = "mypy", specifier = "==1.14.0" }, { name = "mypy-extensions", specifier = "==1.0.0" }, { name = "nodeenv", specifier = "==1.9.1" }, + { name = "pint", specifier = ">=0.24.4" }, { name = "pip", specifier = "==24.3.1" }, { name = "platformdirs", specifier = "==4.3.6" }, { name = "pre-commit", specifier = "==4.0.1" }, @@ -1449,6 +1453,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" @@ -3240,6 +3268,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" From 0b84d95ce354fca165e4a3111743268edc04fcc5 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 14:34:47 +0200 Subject: [PATCH 08/32] Clean up pyproject.toml --- pyproject.toml | 199 +++------------- uv.lock | 625 ++++--------------------------------------------- 2 files changed, 79 insertions(+), 745 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9f8a267..218c2f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,197 +56,52 @@ full = [ dev = [ # Key dependencies # ---------------- - "liccheck==0.9.2", - "mypy==1.14.0", - # Required for liccheck, see https://github.com/dhatim/python-license-check/pull/113 - "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", "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", + # 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", ] 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, diff --git a/uv.lock b/uv.lock index 7c7fe71..8975f32 100644 --- a/uv.lock +++ b/uv.lock @@ -752,329 +752,71 @@ 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 = "fprettify" }, - { 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 = "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 = "fprettify" }, - { name = "identify" }, - { name = "jinja2" }, { 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" }, ] @@ -1087,332 +829,69 @@ 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 = "attrs", specifier = ">=25.3.0" }, { name = "fprettify", specifier = ">=0.3.7" }, - { 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 = "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 = "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 = "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 = "fprettify", specifier = ">=0.3.7" }, - { 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 = "liccheck", specifier = ">=0.9.2" }, + { name = "mypy", specifier = ">=1.14.0" }, { name = "pint", specifier = ">=0.24.4" }, - { 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 = "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" From dc0b7338a2b07234c9b11a84a444fdbe621823fd Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 14:41:54 +0200 Subject: [PATCH 09/32] Try adding no-sync flag --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ea7a499..c92f52e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -97,7 +97,7 @@ 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 --no-sync pytest -r a -v src tests --doctest-modules --cov=src --cov-report=term-missing --cov-report=xml uv run coverage report - name: Upload coverage reports to Codecov with GitHub Action uses: codecov/codecov-action@v4.2.0 From fe24386f32ed2e77365d8f7cb0bdb335937db2c7 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 14:43:12 +0200 Subject: [PATCH 10/32] Add no-editable flag when installing --- .github/actions/setup/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 }} From 10ecbe340e9255847be9ea4b03369fd5e81e1cf9 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 14:45:52 +0200 Subject: [PATCH 11/32] Add attrs as a requirement --- pyproject.toml | 1 + requirements-incl-optional-locked.txt | 1 + requirements-locked.txt | 1 + uv.lock | 4 ++++ 4 files changed, 7 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 218c2f7..dc30a0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ authors = [ license = { text = "placeholder" } requires-python = ">=3.9" dependencies = [ + "attrs>=25.3.0", ] readme = "README.md" classifiers = [ diff --git a/requirements-incl-optional-locked.txt b/requirements-incl-optional-locked.txt index 45f760d..8c7b11e 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' diff --git a/requirements-locked.txt b/requirements-locked.txt index 47e9913..3066cd2 100644 --- a/requirements-locked.txt +++ b/requirements-locked.txt @@ -1,2 +1,3 @@ # 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 diff --git a/uv.lock b/uv.lock index 8975f32..47e1be8 100644 --- a/uv.lock +++ b/uv.lock @@ -739,6 +739,9 @@ wheels = [ name = "example-fgen-basic" version = "0.1.0a1" source = { editable = "." } +dependencies = [ + { name = "attrs" }, +] [package.optional-dependencies] full = [ @@ -822,6 +825,7 @@ tests-min = [ [package.metadata] requires-dist = [ + { name = "attrs", specifier = ">=25.3.0" }, { name = "example-fgen-basic", extras = ["plots"], marker = "extra == 'full'" }, { name = "matplotlib", marker = "extra == 'plots'", specifier = ">=3.7.1" }, ] From 5ad67f89f072bb8f8a724311f2f6bab7ddc150a3 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 14:47:36 +0200 Subject: [PATCH 12/32] Add some hacky diagnostic --- .github/workflows/ci.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c92f52e..3e5f057 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -95,6 +95,9 @@ jobs: # we should add a CI step that runs the tests without optional dependencies too. # We don't have that right now, because we're not sure this pain point exists. uv-dependency-install-flags: "--all-extras --group tests" + - name: Show installed tree + run: | + ls .venv/lib/python3.11/site-packages/example_fgen_basic - name: Run tests run: | uv run --no-sync pytest -r a -v src tests --doctest-modules --cov=src --cov-report=term-missing --cov-report=xml From ed0b4abe0b653a959a164ef567cfa2690e0259d0 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 14:50:38 +0200 Subject: [PATCH 13/32] Try something else for installing --- .github/workflows/ci.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3e5f057..33c05c2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -100,8 +100,9 @@ jobs: ls .venv/lib/python3.11/site-packages/example_fgen_basic - name: Run tests run: | - uv run --no-sync pytest -r a -v src tests --doctest-modules --cov=src --cov-report=term-missing --cov-report=xml - uv run coverage report + # uv run --no-sync pytest -r a -v src tests --doctest-modules --cov=src --cov-report=term-missing --cov-report=xml + uv run --no-editable --reinstall-package example-fgen-basic pytest -r a -v src tests --doctest-modules --cov=src --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: From fb5bf3db6b538b3cc09f5853ac7f399e59d9a964 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 14:54:50 +0200 Subject: [PATCH 14/32] Try more diagnostics --- .github/workflows/ci.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 33c05c2..2802d5a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -101,6 +101,9 @@ jobs: - name: Run tests run: | # uv run --no-sync pytest -r a -v src tests --doctest-modules --cov=src --cov-report=term-missing --cov-report=xml + uv run --no-editable --reinstall-package example-fgen-basic python -c 'import example_fgen_basic;print(example_fgen_basic.__file__)' + cd .venv/lib/python3.11/site-packages/example_fgen_basic + python -c 'import _lib; print("Found it here")' uv run --no-editable --reinstall-package example-fgen-basic pytest -r a -v src tests --doctest-modules --cov=src --cov-report=term-missing --cov-report=xml uv run --no-sync coverage report - name: Upload coverage reports to Codecov with GitHub Action From 924eae736e5b1cb1ce850b434ed742dbd9dbd223 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 14:59:35 +0200 Subject: [PATCH 15/32] Different diagnostics --- .github/workflows/ci.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2802d5a..25badfb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -103,7 +103,9 @@ jobs: # uv run --no-sync pytest -r a -v src tests --doctest-modules --cov=src --cov-report=term-missing --cov-report=xml uv run --no-editable --reinstall-package example-fgen-basic python -c 'import example_fgen_basic;print(example_fgen_basic.__file__)' cd .venv/lib/python3.11/site-packages/example_fgen_basic - python -c 'import _lib; print("Found it here")' + ls + ../../../../bin/python -c 'import exceptions; print("Found exceptions here")' + ../../../../bin/python -c 'import _lib; print("Found _lib here")' uv run --no-editable --reinstall-package example-fgen-basic pytest -r a -v src tests --doctest-modules --cov=src --cov-report=term-missing --cov-report=xml uv run --no-sync coverage report - name: Upload coverage reports to Codecov with GitHub Action From c8f8a4e01c65cfe8c5358c624116e961bf105165 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 15:02:07 +0200 Subject: [PATCH 16/32] More diagnostics --- .github/workflows/ci.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 25badfb..047c959 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -106,6 +106,10 @@ jobs: ls ../../../../bin/python -c 'import exceptions; print("Found exceptions here")' ../../../../bin/python -c 'import _lib; print("Found _lib here")' + cd ../../../../../ + ls + .venv/bin/pytest -r a -v src tests --doctest-modules --cov=src --cov-report=term-missing --cov-report=xml + uv run which pytest uv run --no-editable --reinstall-package example-fgen-basic pytest -r a -v src tests --doctest-modules --cov=src --cov-report=term-missing --cov-report=xml uv run --no-sync coverage report - name: Upload coverage reports to Codecov with GitHub Action From fcda786fe2ee9c244da045357de2104e7a11cb50 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 15:23:58 +0200 Subject: [PATCH 17/32] Identify dependence of coverage on order of test imports --- .github/workflows/ci.yaml | 22 +++++++++++----------- Makefile | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 047c959..9041b30 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -100,17 +100,17 @@ jobs: ls .venv/lib/python3.11/site-packages/example_fgen_basic - name: Run tests run: | - # uv run --no-sync pytest -r a -v src tests --doctest-modules --cov=src --cov-report=term-missing --cov-report=xml - uv run --no-editable --reinstall-package example-fgen-basic python -c 'import example_fgen_basic;print(example_fgen_basic.__file__)' - cd .venv/lib/python3.11/site-packages/example_fgen_basic - ls - ../../../../bin/python -c 'import exceptions; print("Found exceptions here")' - ../../../../bin/python -c 'import _lib; print("Found _lib here")' - cd ../../../../../ - ls - .venv/bin/pytest -r a -v src tests --doctest-modules --cov=src --cov-report=term-missing --cov-report=xml - uv run which pytest - uv run --no-editable --reinstall-package example-fgen-basic pytest -r a -v src tests --doctest-modules --cov=src --cov-report=term-missing --cov-report=xml + uv run --no-sync pytest -r a -v tests src --doctest-modules --doctest-report ndiff --cov=src --cov-report=term-missing --cov-report=xml + # uv run --no-editable --reinstall-package example-fgen-basic python -c 'import example_fgen_basic;print(example_fgen_basic.__file__)' + # cd .venv/lib/python3.11/site-packages/example_fgen_basic + # ls + # ../../../../bin/python -c 'import exceptions; print("Found exceptions here")' + # ../../../../bin/python -c 'import _lib; print("Found _lib here")' + # cd ../../../../../ + # ls + # .venv/bin/pytest -r a -v src tests --doctest-modules --cov=src --cov-report=term-missing --cov-report=xml + # uv run which pytest + # uv run --no-editable --reinstall-package example-fgen-basic pytest -r a -v src tests --doctest-modules --cov=src --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 diff --git a/Makefile b/Makefile index f6b6513..8053465 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,7 @@ ruff-fixes: ## fix the code using ruff .PHONY: test 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) - uv run --no-editable --reinstall-package example-fgen-basic pytest tests src tests -r a -v --doctest-modules --doctest-report ndiff --cov=src + uv run --no-editable --reinstall-package example-fgen-basic pytest -r a -v -tests src -doctest-modules --doctest-report ndiff --cov=src # Note on code coverage and testing: # You must specify cov=src. From 4840c8047b2e7ff6f86f2b3084d62385d0303979 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 15:27:40 +0200 Subject: [PATCH 18/32] Try tricking code coverage --- .github/workflows/ci.yaml | 18 +++++------------- Makefile | 9 +++++++++ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9041b30..bea7cc0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -95,23 +95,15 @@ jobs: # we should add a CI step that runs the tests without optional dependencies too. # We don't have that right now, because we're not sure this pain point exists. uv-dependency-install-flags: "--all-extras --group tests" - - name: Show installed tree - run: | - ls .venv/lib/python3.11/site-packages/example_fgen_basic - name: Run tests run: | + rm -r src/example_fgen_basic + ln -s .venv/lib/python3.11/site-packages/example_fgen_basic src/example_fgen_basic uv run --no-sync pytest -r a -v tests src --doctest-modules --doctest-report ndiff --cov=src --cov-report=term-missing --cov-report=xml - # uv run --no-editable --reinstall-package example-fgen-basic python -c 'import example_fgen_basic;print(example_fgen_basic.__file__)' - # cd .venv/lib/python3.11/site-packages/example_fgen_basic - # ls - # ../../../../bin/python -c 'import exceptions; print("Found exceptions here")' - # ../../../../bin/python -c 'import _lib; print("Found _lib here")' - # cd ../../../../../ - # ls - # .venv/bin/pytest -r a -v src tests --doctest-modules --cov=src --cov-report=term-missing --cov-report=xml - # uv run which pytest - # uv run --no-editable --reinstall-package example-fgen-basic pytest -r a -v src tests --doctest-modules --cov=src --cov-report=term-missing --cov-report=xml uv run --no-sync coverage report + # Just in case, undo the changes to `src` + rm -r src/example_fgen_basic + git restore --staged . && git restore . - name: Upload coverage reports to Codecov with GitHub Action uses: codecov/codecov-action@v4.2.0 env: diff --git a/Makefile b/Makefile index 8053465..322492f 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,15 @@ ruff-fixes: ## fix the code using ruff .PHONY: test 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=src # Note on code coverage and testing: From 36498a01e8844a3861e582d2c6fc7868d1561b03 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 15:34:53 +0200 Subject: [PATCH 19/32] Try tricking code coverage another way --- .github/workflows/ci.yaml | 9 +++------ Makefile | 4 +++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bea7cc0..3c0b50f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -97,13 +97,10 @@ jobs: uv-dependency-install-flags: "--all-extras --group tests" - name: Run tests run: | - rm -r src/example_fgen_basic - ln -s .venv/lib/python3.11/site-packages/example_fgen_basic src/example_fgen_basic - uv run --no-sync pytest -r a -v tests src --doctest-modules --doctest-report ndiff --cov=src --cov-report=term-missing --cov-report=xml + + 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 - # Just in case, undo the changes to `src` - rm -r src/example_fgen_basic - git restore --staged . && git restore . - name: Upload coverage reports to Codecov with GitHub Action uses: codecov/codecov-action@v4.2.0 env: diff --git a/Makefile b/Makefile index 322492f..5f507fd 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,8 @@ 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 @@ -49,7 +51,7 @@ test: ## run the tests (re-installs the package every time so you might want to # 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=src + 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. From a96b1ff1a37d64e5f831767ff00344e07e00e458 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 15:42:05 +0200 Subject: [PATCH 20/32] Turn off code coverage mostly for now --- Makefile | 4 ++-- pyproject.toml | 5 +++++ src/example_fgen_basic/get_wavelength.py | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 5f507fd..8468df0 100644 --- a/Makefile +++ b/Makefile @@ -92,8 +92,8 @@ 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 diff --git a/pyproject.toml b/pyproject.toml index dc30a0d..cfcb590 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -140,6 +140,11 @@ source = [ "src", ] branch = true +omit = [ + # TODO: check this file + "*exceptions.py", + "*runtime_helpers.py", +] [tool.coverage.report] fail_under = 90 diff --git a/src/example_fgen_basic/get_wavelength.py b/src/example_fgen_basic/get_wavelength.py index 80e0042..fe00102 100644 --- a/src/example_fgen_basic/get_wavelength.py +++ b/src/example_fgen_basic/get_wavelength.py @@ -17,7 +17,7 @@ try: from example_fgen_basic._lib import m_get_wavelength_w # type: ignore -except (ModuleNotFoundError, ImportError) as exc: +except (ModuleNotFoundError, ImportError) as exc: # pragma: no cover raise CompiledExtensionNotFoundError("example_fgen_basic._lib") from exc From 6c9701b0453dfda1056a74f0be321f176f4c8bc6 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 15:45:36 +0200 Subject: [PATCH 21/32] Add numpy dependency --- pyproject.toml | 4 +++- requirements-locked.txt | 3 +++ uv.lock | 43 ++++++++++++++++++++++++++++------------- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cfcb590..550102f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,9 @@ authors = [ license = { text = "placeholder" } requires-python = ">=3.9" dependencies = [ - "attrs>=25.3.0", + "attrs>=24.3.0", + "numpy>=1.26.0; python_version < '3.13'", + "numpy>=2.1.0; python_version >= '3.13'", ] readme = "README.md" classifiers = [ diff --git a/requirements-locked.txt b/requirements-locked.txt index 3066cd2..bdf56e5 100644 --- a/requirements-locked.txt +++ b/requirements-locked.txt @@ -1,3 +1,6 @@ # 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' diff --git a/uv.lock b/uv.lock index 47e1be8..7476b6d 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'", ] @@ -515,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'" }, @@ -741,6 +745,9 @@ 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'" }, ] [package.optional-dependencies] @@ -825,9 +832,11 @@ tests-min = [ [package.metadata] requires-dist = [ - { name = "attrs", specifier = ">=25.3.0" }, + { 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" }, ] provides-extras = ["plots", "full"] @@ -1225,8 +1234,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'" }, @@ -1636,9 +1647,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" } @@ -1919,9 +1932,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 = [ @@ -2492,8 +2507,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 = [ From aa2db416d02f6c31f9d1140a4e87d0fe1e44e301 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 15:46:48 +0200 Subject: [PATCH 22/32] Add __vesion__ to package namespace --- src/example_fgen_basic/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/example_fgen_basic/__init__.py b/src/example_fgen_basic/__init__.py index 29c6141..dc0eb97 100644 --- a/src/example_fgen_basic/__init__.py +++ b/src/example_fgen_basic/__init__.py @@ -1,3 +1,9 @@ """ Example of wrapping Fortran so it can be accessed via Python """ + +import importlib.metadata + +__version__ = importlib.metadata.version("example_fgen_basic") + +__all__ = [] From 8ec7db45ad9d5f2b4641bb2d1eb2a7dd61cd0c73 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 15:49:27 +0200 Subject: [PATCH 23/32] Add type hints that work with python 3.9 --- pyproject.toml | 1 + requirements-docs-locked.txt | 2 +- requirements-incl-optional-locked.txt | 1 + requirements-locked.txt | 1 + src/example_fgen_basic/runtime_helpers.py | 3 ++- uv.lock | 2 ++ 6 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 550102f..0d6e8a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ 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 = [ 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 8c7b11e..9855b1c 100644 --- a/requirements-incl-optional-locked.txt +++ b/requirements-incl-optional-locked.txt @@ -19,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 bdf56e5..3502dc3 100644 --- a/requirements-locked.txt +++ b/requirements-locked.txt @@ -4,3 +4,4 @@ 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/src/example_fgen_basic/runtime_helpers.py b/src/example_fgen_basic/runtime_helpers.py index abe32d7..3249812 100644 --- a/src/example_fgen_basic/runtime_helpers.py +++ b/src/example_fgen_basic/runtime_helpers.py @@ -9,10 +9,11 @@ from abc import ABC, abstractmethod from collections.abc import Iterable from functools import wraps -from typing import Any, Callable, Concatenate, ParamSpec, TypeVar +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 diff --git a/uv.lock b/uv.lock index 7476b6d..70a2632 100644 --- a/uv.lock +++ b/uv.lock @@ -748,6 +748,7 @@ dependencies = [ { 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] @@ -837,6 +838,7 @@ requires-dist = [ { 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"] From 3077ae4c0ab81768543f084efd8406e96a77c28c Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 15:56:09 +0200 Subject: [PATCH 24/32] Update mypy and add --no-sync throughout ci.yaml --- .github/workflows/ci.yaml | 12 ++++++------ Makefile | 4 ++-- src/example_fgen_basic/__init__.py | 2 -- src/example_fgen_basic/get_wavelength.py | 2 +- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3c0b50f..c596e26 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 . @@ -230,7 +230,7 @@ jobs: python-version: ${{ matrix.python-version }} - 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` @@ -258,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/Makefile b/Makefile index 8468df0..b57d1bd 100644 --- a/Makefile +++ b/Makefile @@ -28,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 diff --git a/src/example_fgen_basic/__init__.py b/src/example_fgen_basic/__init__.py index dc0eb97..64df7f1 100644 --- a/src/example_fgen_basic/__init__.py +++ b/src/example_fgen_basic/__init__.py @@ -5,5 +5,3 @@ import importlib.metadata __version__ = importlib.metadata.version("example_fgen_basic") - -__all__ = [] diff --git a/src/example_fgen_basic/get_wavelength.py b/src/example_fgen_basic/get_wavelength.py index fe00102..c6127a6 100644 --- a/src/example_fgen_basic/get_wavelength.py +++ b/src/example_fgen_basic/get_wavelength.py @@ -35,7 +35,7 @@ def get_wavelength_plain(frequency: float) -> float: : Wavelength of light for given `frequency` """ - res = m_get_wavelength_w.get_wavelength(frequency) + res: float = m_get_wavelength_w.get_wavelength(frequency) return res From 79f19531130e8a5701ea3d27aef6b4c0aad53fa1 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 15:56:51 +0200 Subject: [PATCH 25/32] Add no sync throughout GHA --- .github/workflows/bump.yaml | 2 +- .github/workflows/deploy.yaml | 2 +- .github/workflows/release.yaml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) 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/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" From 948cd9a865fc5a4c489dffcf4ef3e2606e6c5ad7 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 16:03:18 +0200 Subject: [PATCH 26/32] Fix build --- .github/workflows/ci.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c596e26..69555a3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -222,12 +222,10 @@ 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 --no-sync python scripts/add-locked-targets-to-pyproject-toml.py From ca0d5f379a33037f7ad3509e8390e6379be65617 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 16:15:10 +0200 Subject: [PATCH 27/32] CHANGELOG --- changelog/2.feature.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/2.feature.md 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 From 94f991e7a32db447e4b805dae89989fd52cf1d14 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 16:18:41 +0200 Subject: [PATCH 28/32] Update URL checks --- .github/workflows/ci.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 69555a3..e70e896 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -65,7 +65,8 @@ 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 + args: "--exclude 'file://' --exclude '^https://github\\.com/openscm/example-fgen-basic/pull/\\{issue\\}$' --exclude '^https://github.com/conda-forge/example-fgen-basic-feedstock$' ." tests: strategy: From cb1132601c7870f08e38a4564b84da8b6b6c5b53 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 16:19:58 +0200 Subject: [PATCH 29/32] Ignore rtd from URL checks too --- .github/workflows/ci.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e70e896..79f9590 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -66,7 +66,8 @@ jobs: # Exclude local links # and the template link in pyproject.toml # Don't check for conda-forge while we haven't released there - args: "--exclude 'file://' --exclude '^https://github\\.com/openscm/example-fgen-basic/pull/\\{issue\\}$' --exclude '^https://github.com/conda-forge/example-fgen-basic-feedstock$' ." + # 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: From e94fc3d9ee93a9232fad385f0f3d5ee0139b9619 Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 16:31:58 +0200 Subject: [PATCH 30/32] Avoid writing docs for extension module --- Makefile | 6 +++--- docs/gen_doc_stubs.py | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index b57d1bd..94e8b82 100644 --- a/Makefile +++ b/Makefile @@ -68,15 +68,15 @@ test: ## run the tests (re-installs the package every time so you might want to .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 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) From 44760457718f5103512789b73e44f46a6fc61ead Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 16:40:18 +0200 Subject: [PATCH 31/32] Try updating RtD build --- .readthedocs.yaml | 8 +++++--- environment-docs-conda-base.yml | 10 ++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 environment-docs-conda-base.yml 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/environment-docs-conda-base.yml b/environment-docs-conda-base.yml new file mode 100644 index 0000000..170019e --- /dev/null +++ b/environment-docs-conda-base.yml @@ -0,0 +1,10 @@ +name: fortran-compilers + +channels: + - conda-forge + - defaults + +dependencies: + - python=3.11 + - gcc + - gfortran From 107a66f34e4daabd5a3a125e1576d8f3949b099b Mon Sep 17 00:00:00 2001 From: Zebedee Nicholls Date: Thu, 14 Aug 2025 16:43:47 +0200 Subject: [PATCH 32/32] Add explicit pip dependency for RtD --- environment-docs-conda-base.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/environment-docs-conda-base.yml b/environment-docs-conda-base.yml index 170019e..0d2d79b 100644 --- a/environment-docs-conda-base.yml +++ b/environment-docs-conda-base.yml @@ -6,5 +6,6 @@ channels: dependencies: - python=3.11 + - pip - gcc - gfortran