Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/scripts/check_tool_pins.py
Original file line number Diff line number Diff line change
@@ -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())
18 changes: 17 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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' }}
Expand Down Expand Up @@ -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: >-
Expand Down
25 changes: 24 additions & 1 deletion docs/content/docs/contributing/dependencies.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
11 changes: 9 additions & 2 deletions prek.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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 },
]
14 changes: 7 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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",
Expand All @@ -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" },
]
Expand Down
24 changes: 12 additions & 12 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading