From 6622280be9d0019f487c7586586307c8e34d662a Mon Sep 17 00:00:00 2001 From: Panadestein Date: Tue, 6 Oct 2026 15:17:36 +0200 Subject: [PATCH] =?UTF-8?q?chore(deps):=20=F0=9F=94=A7=20relax=20dependenc?= =?UTF-8?q?y=20floors=20and=20test=20them=20in=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit numpy goes from >=2.4.4 to >=2.0 and quimb from >=1.14.0 to >=1.8.3, the oldest releases the suite passes on (quimb 1.8.0-1.8.2 ignore float32 in MPO_rand). A new test job on Python 3.11 resolves every direct dependency to its lowest allowed version so the floors stay tested. A check-tool-pins prek hook fails if the ruff or ty pins in pyproject.toml and prek.toml disagree. Closes #18 Assisted-by: pi:claude-opus-5.5 --- .github/scripts/check_tool_pins.py | 58 +++++++++++++++++++ .github/workflows/test.yml | 18 +++++- .../docs/contributing/dependencies.mdx | 25 +++++++- prek.toml | 11 +++- pyproject.toml | 14 ++--- uv.lock | 24 ++++---- 6 files changed, 127 insertions(+), 23 deletions(-) create mode 100644 .github/scripts/check_tool_pins.py diff --git a/.github/scripts/check_tool_pins.py b/.github/scripts/check_tool_pins.py new file mode 100644 index 0000000..a3d54b9 --- /dev/null +++ b/.github/scripts/check_tool_pins.py @@ -0,0 +1,58 @@ +# ruff: noqa: INP001 (a standalone script, not part of a package) +"""Fail if a tool is pinned to different versions in pyproject.toml and prek.toml. + +`uv run ruff` and the prek hook must agree, or a file can pass locally and fail +in CI. Each tool below is pinned as ``tool==X.Y.Z`` in a dependency group and as +``rev = "vX.Y.Z"`` on its pre-commit mirror. +""" + +from __future__ import annotations + +import re +import sys +import tomllib +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +MIRRORS = { + "ruff": "https://github.com/astral-sh/ruff-pre-commit", + "ty": "https://github.com/astral-sh/ty-pre-commit", +} + + +def main() -> int: + """Compare the pins and report every mismatch. + + Returns: + The process exit code: 0 if every pin agrees, 1 otherwise. + """ + pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text()) + prek = tomllib.loads((ROOT / "prek.toml").read_text()) + requirements = [ + req + for group in pyproject["dependency-groups"].values() + for req in group + if isinstance(req, str) + ] + revs = {repo["repo"]: repo.get("rev") for repo in prek["repos"]} + + errors = [] + for tool, mirror in MIRRORS.items(): + pins = { + m.group(1) + for req in requirements + if (m := re.fullmatch(rf"{tool}==(\S+)", req.replace(" ", ""))) + } + rev = revs.get(mirror) + if len(pins) != 1 or rev is None: + errors.append(f"{tool}: expected one '{tool}==' pin and a {mirror} rev") + elif rev.removeprefix("v") != (pin := pins.pop()): + errors.append(f"{tool}: pyproject.toml pins {pin}, prek.toml pins {rev}") + + for error in errors: + print(error, file=sys.stderr) # noqa: T201 + return 1 if errors else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 72bb285..791ce51 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,9 +46,24 @@ jobs: - "3.14" test-group: - "not slow and not perf" + resolution: + - "highest" + include: + # The declared dependency floors, so they are tested rather than asserted. + - runner: ubuntu-latest + python-version: "3.11" + test-group: "not slow and not perf" + resolution: "lowest-direct" runs-on: ${{ matrix.runner }} + env: + # Applies to every uv call, so the lockfile is resolved the same way each time. + UV_RESOLUTION: ${{ matrix.resolution }} + # `uv run` uses the environment `uv sync` built below instead of adding the + # `dev` group, which at its lowest versions need not even build. + UV_NO_SYNC: "1" + steps: - name: Runner hardware info Linux if: ${{ runner.os == 'linux' }} @@ -105,7 +120,8 @@ jobs: run: | runner="${{ matrix.runner }}" pyver="${{ matrix.python-version }}" - echo "label=$runner-$pyver" >> $GITHUB_OUTPUT + resolution="${{ matrix.resolution }}" + echo "label=$runner-$pyver-$resolution" >> $GITHUB_OUTPUT - name: Run tests run: >- diff --git a/docs/content/docs/contributing/dependencies.mdx b/docs/content/docs/contributing/dependencies.mdx index 890481b..dea8cb9 100644 --- a/docs/content/docs/contributing/dependencies.mdx +++ b/docs/content/docs/contributing/dependencies.mdx @@ -33,6 +33,29 @@ Dependencies are managed with [uv](https://docs.astral.sh/uv/) and declared in 2. Run `uv sync --all-groups` to update the environment and regenerate `uv.lock`, and commit both files. The `uv-lock` hook fails if they disagree. +## Version constraints + Declare the oldest version that works rather than the one you happen to have installed: a high floor stops users from installing `src_method` next to anything -that has not caught up yet. +that has not caught up yet. Give every direct dependency a floor, including those +in dependency groups. + +Floors are tested rather than assumed: the test workflow has a job on the oldest +supported Python that resolves every direct dependency to its lowest allowed +version (`UV_RESOLUTION=lowest-direct`). To reproduce it locally: + +```bash +export UV_RESOLUTION=lowest-direct +uv sync --python 3.11 --no-dev --group test +uv run --no-sync pytest -m "not slow" +``` + +`--no-sync` matters: a plain `uv run` would also install the `dev` group, at its +lowest versions. Do not commit the `uv.lock` this produces. Raise a floor only +when the code needs a newer feature or the lowest-resolution job fails. + +## Tool pins + +`ruff` and `ty` are pinned twice, in the `dev` group and in `prek.toml`. The +`check-tool-pins` hook fails if the two disagree; `update-ruff.yml` and +`update-ty.yml` bump both pins of their tool together. diff --git a/prek.toml b/prek.toml index 1401989..680dcdb 100644 --- a/prek.toml +++ b/prek.toml @@ -26,7 +26,8 @@ rev = "v5.0.0" hooks = [{ id = "forbid-submodules" }] [[repos]] -# Keep in sync with the ruff pin in pyproject.toml; update-ruff.yml bumps both. +# Keep in sync with the ruff pin in pyproject.toml; update-ruff.yml bumps both +# and check-tool-pins enforces it. repo = "https://github.com/astral-sh/ruff-pre-commit" rev = "v0.16.9" hooks = [ @@ -43,7 +44,13 @@ hooks = [{ id = "uv-lock" }] [[repos]] -# Keep in sync with the ty pin in pyproject.toml. +# Keep in sync with the ty pin in pyproject.toml; check-tool-pins enforces it. repo = "https://github.com/astral-sh/ty-pre-commit" rev = "v0.0.82" hooks = [{ id = "ty" }] + +[[repos]] +repo = "local" +hooks = [ + { id = "check-tool-pins", name = "ruff and ty pins agree", language = "python", entry = "python .github/scripts/check_tool_pins.py", files = "^(pyproject|prek)\\.toml$", pass_filenames = false }, +] diff --git a/pyproject.toml b/pyproject.toml index 5dcf6a2..b4f80a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ classifiers = [ "Typing :: Typed", ] dynamic = ["version"] -dependencies = ["numpy>=2.4.4,<3", "opt_einsum>=3.4.0"] +dependencies = ["numpy>=2.0,<3", "opt_einsum>=3.4.0"] [project.optional-dependencies] @@ -63,15 +63,15 @@ test = [ "pytest-cov>=4.0", "pytest>=9.1.0,<10.0", "pytest-benchmark>=5.2.3", - "quimb>=1.14.0,<2", + "quimb>=1.8.3,<2", # Not imported directly: without it, cotengra (via quimb) warns when it plans a # contraction, which `filterwarnings = ["error"]` turns into a failure. "cmaes>=0.12.0", ] # Only the scripts under benches/ need these. -bench = ["cyclopts>=3.20", "quimb>=1.14.0,<2", "cmaes>=0.12.0", "scipy"] +bench = ["cyclopts>=3.20", "quimb>=1.8.3,<2", "cmaes>=0.12.0", "scipy>=1.13"] dev = [ - "scalene", + "scalene>=1.5", "prek>=0.2.6", "ruff==0.16.9", "ty==0.0.82", @@ -83,13 +83,13 @@ interactive = [ "matplotlib>=3.11.0", "pydot>=1.4", "tqdm>=4.65.0", - "pylatexenc", + "pylatexenc>=2.10", ] docs = [ # Builds the Fumadocs site under docs/; see docs/README.md. "griffelib>=2.2,<3", - "nbconvert", - "pytest-markdown-docs", + "nbconvert>=7", + "pytest-markdown-docs>=0.9", { include-group = "test" }, { include-group = "interactive" }, ] diff --git a/uv.lock b/uv.lock index 6b66093..ef2c258 100644 --- a/uv.lock +++ b/uv.lock @@ -2439,7 +2439,7 @@ test = [ requires-dist = [ { name = "cupy", marker = "extra == 'gpu-rocm'", specifier = ">=14.1.1" }, { name = "cupy-cuda12x", marker = "extra == 'gpu-nvidia'", specifier = ">=13" }, - { name = "numpy", specifier = ">=2.4.4,<3" }, + { name = "numpy", specifier = ">=2.0,<3" }, { name = "nvidia-cublas-cu12", marker = "extra == 'gpu-nvidia'" }, { name = "nvidia-cuda-nvrtc-cu12", marker = "extra == 'gpu-nvidia'" }, { name = "nvidia-cuda-runtime-cu12", marker = "extra == 'gpu-nvidia'" }, @@ -2455,8 +2455,8 @@ provides-extras = ["gpu-nvidia", "gpu-rocm"] bench = [ { name = "cmaes", specifier = ">=0.12.0" }, { name = "cyclopts", specifier = ">=3.20" }, - { name = "quimb", specifier = ">=1.14.0,<2" }, - { name = "scipy" }, + { name = "quimb", specifier = ">=1.8.3,<2" }, + { name = "scipy", specifier = ">=1.13" }, ] dev = [ { name = "cmaes", specifier = ">=0.12.0" }, @@ -2465,10 +2465,10 @@ dev = [ { name = "pytest", specifier = ">=9.1.0,<10.0" }, { name = "pytest-benchmark", specifier = ">=5.2.3" }, { name = "pytest-cov", specifier = ">=4.0" }, - { name = "quimb", specifier = ">=1.14.0,<2" }, + { name = "quimb", specifier = ">=1.8.3,<2" }, { name = "ruff", specifier = "==0.16.9" }, - { name = "scalene" }, - { name = "scipy" }, + { name = "scalene", specifier = ">=1.5" }, + { name = "scipy", specifier = ">=1.13" }, { name = "ty", specifier = "==0.0.82" }, ] docs = [ @@ -2476,21 +2476,21 @@ docs = [ { name = "griffelib", specifier = ">=2.2,<3" }, { name = "ipykernel", specifier = ">=7.2.0" }, { name = "matplotlib", specifier = ">=3.11.0" }, - { name = "nbconvert" }, + { name = "nbconvert", specifier = ">=7" }, { name = "pydot", specifier = ">=1.4" }, - { name = "pylatexenc" }, + { name = "pylatexenc", specifier = ">=2.10" }, { name = "pytest", specifier = ">=9.1.0,<10.0" }, { name = "pytest-benchmark", specifier = ">=5.2.3" }, { name = "pytest-cov", specifier = ">=4.0" }, - { name = "pytest-markdown-docs" }, - { name = "quimb", specifier = ">=1.14.0,<2" }, + { name = "pytest-markdown-docs", specifier = ">=0.9" }, + { name = "quimb", specifier = ">=1.8.3,<2" }, { name = "tqdm", specifier = ">=4.65.0" }, ] interactive = [ { name = "ipykernel", specifier = ">=7.2.0" }, { name = "matplotlib", specifier = ">=3.11.0" }, { name = "pydot", specifier = ">=1.4" }, - { name = "pylatexenc" }, + { name = "pylatexenc", specifier = ">=2.10" }, { name = "tqdm", specifier = ">=4.65.0" }, ] test = [ @@ -2498,7 +2498,7 @@ test = [ { name = "pytest", specifier = ">=9.1.0,<10.0" }, { name = "pytest-benchmark", specifier = ">=5.2.3" }, { name = "pytest-cov", specifier = ">=4.0" }, - { name = "quimb", specifier = ">=1.14.0,<2" }, + { name = "quimb", specifier = ">=1.8.3,<2" }, ] [[package]]