From 715f95aa2b6053d2b1497510bd0e6568fb1058f3 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 14 Aug 2026 15:05:59 +0200 Subject: [PATCH] feat(just): pin the test interpreter and fan out over python versions locally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `just test` took no interpreter, so which python it ran on was ambient state: whatever the project environment happened to hold. CI got the right answer only because the preceding `uv sync --python` step had shaped that environment, and locally there was no way to reproduce a single matrix leg at all. Add a `py` variable that pins the interpreter in the recipe itself, and a `test-all` recipe that loops over the supported versions. A pinned version gets its own environment directory. `uv run --python` does not merely select an interpreter: when the project environment's interpreter does not match, uv deletes and rebuilds it. Without this, the advertised `just py=3.12 test` would destroy the `.venv` the developer was working in and strand it on the pinned version — and for zarr-indexing, whose recipes run against the repo-root project, the casualty would be the root zarr environment. CI is unaffected either way, since its `uv sync --python` has already shaped the environment to the matrix version. The version lists stay literal, mirroring the classifiers in pyproject.toml and the workflow matrices. Deriving them by scraping the classifiers was tried and dropped: the list feeds only `test-all`, so a bad scrape could not affect what CI covers, and a regex over TOML can produce a wrong-but-plausible list where a literal can only be stale. Assisted-by: ClaudeCode:claude-opus-5 --- .github/workflows/zarr-http-server.yml | 4 ++- .github/workflows/zarr-indexing.yml | 8 +++--- .github/workflows/zarr-metadata.yml | 4 ++- .gitignore | 2 ++ packages/zarr-http-server/justfile | 26 +++++++++++++++++++- packages/zarr-indexing/justfile | 34 ++++++++++++++++++++++++-- packages/zarr-metadata/justfile | 26 +++++++++++++++++++- 7 files changed, 95 insertions(+), 9 deletions(-) diff --git a/.github/workflows/zarr-http-server.yml b/.github/workflows/zarr-http-server.yml index 16589f0d7d..8c6cc9f605 100644 --- a/.github/workflows/zarr-http-server.yml +++ b/.github/workflows/zarr-http-server.yml @@ -60,7 +60,9 @@ jobs: # instead of silently skipping. run: uv sync --group test --group examples --python ${{ matrix.python-version }} - name: Run pytest - run: just test + # `py=` pins the interpreter in the recipe itself, so the matrix leg + # does not depend on the sync step above having shaped `.venv`. + run: just py=${{ matrix.python-version }} test ruff: name: ruff diff --git a/.github/workflows/zarr-indexing.yml b/.github/workflows/zarr-indexing.yml index 61776df913..ac14efaf64 100644 --- a/.github/workflows/zarr-indexing.yml +++ b/.github/workflows/zarr-indexing.yml @@ -51,10 +51,12 @@ jobs: - name: Sync test dependency group run: uv sync --project ../.. --group test --python ${{ matrix.python-version }} - name: Run pytest - # Suites and invocation live in packages/zarr-indexing/justfile. - run: just test + # Suites and invocation live in packages/zarr-indexing/justfile. `py=` + # pins the interpreter in the recipe itself, so the matrix leg does not + # depend on the sync step above having shaped the environment. + run: just py=${{ matrix.python-version }} test - name: Run pytest (tensorstore parity) - run: just test-tensorstore + run: just py=${{ matrix.python-version }} test-tensorstore ruff: name: ruff diff --git a/.github/workflows/zarr-metadata.yml b/.github/workflows/zarr-metadata.yml index 5b3b83b0e0..3e2c316d94 100644 --- a/.github/workflows/zarr-metadata.yml +++ b/.github/workflows/zarr-metadata.yml @@ -49,7 +49,9 @@ jobs: - name: Sync test dependency group run: uv sync --group test --python ${{ matrix.python-version }} - name: Run pytest - run: just test + # `py=` pins the interpreter in the recipe itself, so the matrix leg + # does not depend on the sync step above having shaped `.venv`. + run: just py=${{ matrix.python-version }} test ruff: name: ruff diff --git a/.gitignore b/.gitignore index 59b6632a3c..5c0c5fbaed 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ __pycache__/ .Python env/ .venv/ +# Per-version environments created by the packages' `just test-all` +.venv-*/ build/ develop-eggs/ dist/ diff --git a/packages/zarr-http-server/justfile b/packages/zarr-http-server/justfile index d65b1b53b7..62b7f2929e 100644 --- a/packages/zarr-http-server/justfile +++ b/packages/zarr-http-server/justfile @@ -12,6 +12,21 @@ # with no code change to blame. Bump alongside the pre-commit rev. ruff_version := "0.16.0" +# The python versions `test-all` covers. Mirrors the `Programming Language :: +# Python` classifiers in pyproject.toml and the matrix in +# .github/workflows/zarr-http-server.yml; update the three together. +python_versions := "3.12 3.13 3.14" + +# Interpreter for `test`. Empty means "whatever the project environment already +# holds"; set it to reproduce one CI leg, e.g. `just py=3.12 test`. +py := "" + +# `uv run --python` does not merely select an interpreter: it deletes and +# rebuilds a project environment whose interpreter does not match. So a pinned +# version gets its own environment directory, rather than clobbering the +# `.venv` the developer was using and stranding it on the pinned version. +uv_run := if py == "" { "uv run" } else { "env UV_PROJECT_ENVIRONMENT=.venv-" + py + " uv run --python " + py } + # List available recipes default: @just --list @@ -21,7 +36,16 @@ default: # silently skipping. # Run the test suite; extra args are passed to pytest test *args: - uv run --group test --group examples pytest tests {{ args }} + {{ uv_run }} --group test --group examples pytest tests {{ args }} + +# Run the test suite on every supported python +test-all *args: + #!/usr/bin/env bash + set -euo pipefail + for v in {{ python_versions }}; do + echo "=== python $v ===" + just py="$v" test {{ args }} + done # Lint the package sources and tests lint: diff --git a/packages/zarr-indexing/justfile b/packages/zarr-indexing/justfile index 1b7164f647..41fbbda15b 100644 --- a/packages/zarr-indexing/justfile +++ b/packages/zarr-indexing/justfile @@ -1,6 +1,25 @@ # Development verbs for the zarr-indexing package. Recipes run with this # directory as the working directory regardless of where `just` is invoked. +# The python versions `test-all` covers. Mirrors the `Programming Language :: +# Python` classifiers in pyproject.toml and the matrix in +# .github/workflows/zarr-indexing.yml; update the three together. +python_versions := "3.12 3.13 3.14" + +# Interpreter for the test recipes. Empty means "whatever the project +# environment already holds"; set it to reproduce one CI leg, e.g. +# `just py=3.12 test`. +py := "" + +# `uv run --python` does not merely select an interpreter: it deletes and +# rebuilds a project environment whose interpreter does not match. Since these +# recipes run against the repo-root project, an unguarded pin would clobber the +# root `.venv` — the main zarr development environment. A pinned version gets +# its own directory instead; relative paths resolve against the project root, +# so those land beside the root `.venv` rather than in this package. +uv_run := "uv run --project ../.." + if py == "" { "" } else { " --python " + py } +uv_env := if py == "" { "" } else { "env UV_PROJECT_ENVIRONMENT=.venv-" + py + " " } + # List available recipes default: @just --list @@ -13,7 +32,7 @@ default: # invocation CI uses. # Run the test suite; extra args are passed to pytest test *args: - uv run --project ../.. --group test --with-editable . python -m pytest tests src/zarr_indexing {{ args }} + {{ uv_env }}{{ uv_run }} --group test --with-editable . python -m pytest tests src/zarr_indexing {{ args }} # TensorStore is the oracle for the parity suites, which skip without it. It # ships binary wheels only, so it rides in as a run-time overlay rather than @@ -21,7 +40,18 @@ test *args: # gate the CI job that calls this on the matrix version. # Run the tensorstore parity suites; extra args are passed to pytest test-tensorstore *args: - uv run --project ../.. --group test --with-editable . --with 'tensorstore>=0.1.84' python -m pytest tests/test_ndsel_tensorstore.py tests/test_tensorstore_parity.py {{ args }} + {{ uv_env }}{{ uv_run }} --group test --with-editable . --with 'tensorstore>=0.1.84' python -m pytest tests/test_ndsel_tensorstore.py tests/test_tensorstore_parity.py {{ args }} + +# The tensorstore parity suites are not looped here — they pull a large wheel +# per version; run `just py=3.13 test-tensorstore` for a specific one. +# Run the test suite on every supported python +test-all *args: + #!/usr/bin/env bash + set -euo pipefail + for v in {{ python_versions }}; do + echo "=== python $v ===" + just py="$v" test {{ args }} + done # Lint with the same invocation CI uses. Ruff is pinned to the repo-wide # version (see pyproject.toml [dependency-groups] docs); bump together. diff --git a/packages/zarr-metadata/justfile b/packages/zarr-metadata/justfile index 0f1861ed7d..a1c015cf31 100644 --- a/packages/zarr-metadata/justfile +++ b/packages/zarr-metadata/justfile @@ -1,13 +1,37 @@ # Development verbs for the zarr-metadata package. Recipes run with this # directory as the working directory regardless of where `just` is invoked. +# The python versions `test-all` covers. Mirrors the `Programming Language :: +# Python` classifiers in pyproject.toml and the matrix in +# .github/workflows/zarr-metadata.yml; update the three together. +python_versions := "3.11 3.12 3.13 3.14" + +# Interpreter for `test`. Empty means "whatever the project environment already +# holds"; set it to reproduce one CI leg, e.g. `just py=3.11 test`. +py := "" + +# `uv run --python` does not merely select an interpreter: it deletes and +# rebuilds a project environment whose interpreter does not match. So a pinned +# version gets its own environment directory, rather than clobbering the +# `.venv` the developer was using and stranding it on the pinned version. +uv_run := if py == "" { "uv run" } else { "env UV_PROJECT_ENVIRONMENT=.venv-" + py + " uv run --python " + py } + # List available recipes default: @just --list # Run the test suite; extra args are passed to pytest test *args: - uv run --group test pytest tests {{ args }} + {{ uv_run }} --group test pytest tests {{ args }} + +# Run the test suite on every supported python +test-all *args: + #!/usr/bin/env bash + set -euo pipefail + for v in {{ python_versions }}; do + echo "=== python $v ===" + just py="$v" test {{ args }} + done # Lint with the same invocation CI uses lint: