Skip to content

Move pre-commit to ruff and run fast tests on Python 3.13 and 3.14 - #267

Merged
junpenglao merged 8 commits into
mainfrom
ruff-py313-py314
Oct 4, 2026
Merged

junpenglao merged 8 commits into
mainfrom
ruff-py313-py314

Conversation

@junpenglao

Copy link
Copy Markdown
Member

What

  • ruff replaces the old lint hooks. ruff-check --fix and ruff-format replace isort, pyupgrade, flake8, black and nbQA in pre-commit. The local no-internal-refs hook is unchanged.
  • Remaining hooks bumped: pre-commit-hooks v5.0.0 → v6.0.0, mypy v1.13.0 → v2.4.0 (args kept).
  • Fast-test CI runs on Python 3.13 and 3.14. requires-python was already >=3.13.

This is part of the ecosystem move to ruff with a 3.13/3.14 test matrix. The matching PRs are blackjax-devs/blackjax#1055 and blackjax-devs/sampling-book#100.

Lint scope is unchanged

  • The ruff config selects E, F, W, I, UP (targeting py313) and ignores E501 and E731 as before.
  • Rewrites skipped because they change runtime behaviour:
    • the PEP 695 type and generics rewrites (UP040/046/047);
    • UP042 (str+Enum → StrEnum): it changes str(Effort.LOW) from "Effort.LOW" to "low". tests/recipes/test_schema.py asserts the current form, and _resolve_execution_plan.py builds a file path from an f-string of the enum value.
  • Markdown: ruff-format only runs on python/pyi/jupyter, because its default would also reformat Python code blocks in the catalog READMEs and schemas.
  • nbQA never linted the .md docs. v1.9.0's hooks do match markdown, but on main nbqa-black and nbqa-flake8 print "No notebooks found in given path(s)" for all 33 .md files. The hook environment lacks jupytext, which nbQA needs to read markdown. So dropping nbQA loses nothing.
  • The old flake8 exclude: ^scripts/ had no effect: scripts/ contains no tracked .py files.

Commits

  1. CI: fast-suite matrix 3.13/3.14.
  2. Hook config: pre-commit config, [tool.ruff] replacing [tool.isort], CONTRIBUTING.
  3. ruff format, mechanical: 71 files, all with identical syntax trees. The author agent checked this and I re-checked it independently.
  4. ruff check --fix: UP017 ×7, F401 ×1.
  5. Hand fixes: E741 ×4 renames, one F841 dead variable, UP031 ×4 (% → f-string). UP042 is ignored, with the evidence in a comment.
  6. Markdown scope: limit ruff-format to Python files.
  7. mypy 2.4 fix: an explicit -> Recipe return annotation. Newer mypy narrowed an untyped helper's Any to object.

Checks run locally

  • pre-commit run --all-files passes all 8 hooks under both Python 3.13 and Python 3.14.
  • Fast suite (pytest tests -m fast -n 2):
Python Passed Skipped Time
3.13 2089 5 238 s
3.14 2089 5 243 s
  • No bench-group dependency lacks a 3.14 wheel.

Implemented by the swe agent; reviewed by the TL agent, who re-ran the syntax-tree check and gitlint.

🤖 Generated with Claude Code

junpenglao and others added 8 commits October 4, 2026 16:10
Finding: requires-python was already >=3.13 (no floor change needed),
but test-fast.yml only ran the fast suite on 3.13 and its header
comment was already stale (said 3.11+3.13 while the matrix only had
3.13).
Fix: matrix is now ['3.13', '3.14']; updated both the top-of-file
comment and the inline matrix comment to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Finding: the lint stack carried five separate tools (isort, pyupgrade,
flake8, black, nbQA) with duplicated ignore lists across the main
flake8 hook and nbqa-flake8; the versions were due for a bump anyway
(pre-commit-hooks v5->v6, mypy v1.13->v2.4) ahead of the 3.14 matrix.
Fix: ruff-check (--fix) + ruff-format replace all five under one
pre-commit repo and one [tool.ruff] config; the old flake8 ignore list
(E501, E731) carries over as ruff ignores, plus the three PEP 695
pyupgrade rewrites (UP040/046/047) that change runtime semantics.
isort's --profile black becomes known-first-party under
[tool.ruff.lint.isort]. Updated the two remaining doc references
(pre-commit.yml header comment, CONTRIBUTING.md step 5) that still
named the old tools.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Finding: ruff format and black disagree on a handful of stylistic
calls (trailing comma placement, some multi-line call wrapping).
Fix: ran `ruff format` over all 299 tracked .py files; 71 reformatted,
228 unchanged. Proved the change is purely cosmetic: for every changed
file, ast.dump(ast.parse(old)) == ast.dump(ast.parse(new)) — 71 files,
all AST-equal (script: scratchpad/pysupport/tuningfork/ast_equal_check.py).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Finding: `ruff check --statistics` on the formatted tree reported 22
errors across 6 rules (UP017 x7, UP042 x5, E741 x4, UP031 x4, F401 x1,
F841 x1); 8 were marked safely fixable. Running --fix iteratively
resolved 16 of the 22 (the extra 8 beyond the initial preview are
import-cleanup fixes cascading from the datetime.timezone.utc ->
datetime.UTC rewrite), leaving 14 genuine lint findings for the next
commit.
Fix: `datetime.timezone.utc` -> `datetime.UTC` (UP017, Python 3.11+
alias) across 5 files; dropped one now-provably-unused `matplotlib`
import in the marimo catalog explorer (F401). No behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Finding: 14 of 30 ruff-check findings needed judgment, not --fix.
UP042 (str+Enum -> StrEnum) is NOT safe here: empirically
str(Effort.LOW) is "Effort.LOW" under (str, Enum) but "low" under
StrEnum, and tests/recipes/test_schema.py:61 asserts the former while
tuningfork/recipes/_resolve_execution_plan.py:323 f-strings an Effort
value into a file path — rewriting the 5 flagged classes would be a
silent behavior change, not a style fix.
Fix: E741 ambiguous `l` renamed to `line`/`lam_i` at its 2 sites
(test_emit_x64_timing.py, _execution_telemetry.py); F841 dropped an
unused `n_warmup` local in _timing.py; UP031 converted 4 %-format code
strings to f-strings in _emit/_init_strategy.py (output text is
unchanged — verified by the existing init-strategy/timing/telemetry
test files, 150 passed). UP042 added to the ignore list alongside the
existing PEP 695 out-of-scope rewrites, with the str()/f-string
evidence recorded in the comment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Finding: a live --all-files run showed ruff-format's new default
types_or includes "markdown" (astral-sh/ruff-pre-commit v0.16.10
hook definition) — it rewrote 6 docs (CONTRIBUTING.md, README.md, 3
catalog READMEs/schema, mclmc-routing-taxonomy.md), stripping
hand-aligned inline comments in the Recipe dataclass field table and
collapsing docstring examples. Verified against the OLD stack on a
scratch worktree of main (8bb7081): nbQA 1.9.0's hooks also declare
types_or: [jupyter, markdown], so pre-commit DID hand nbqa-black /
nbqa-flake8 tuningfork's 33 tracked .md files — but both reported
"No notebooks found in given path(s)" (0.15-0.16s, 0 files). Root
cause in nbqa's own source (nbqa/__main__.py): a .md path is only
treated as a notebook if jupytext is importable in the hook's
isolated env, and none of the old nbqa-black/-flake8/-isort/
-pyupgrade hooks declared jupytext as an additional_dependency — so
markdown was already a complete no-op under the old stack, for a
different reason (missing jupytext) than ruff-format's scoping.
Fix: pin types_or: [python, pyi, jupyter] on the ruff-format hook so
coverage matches the old (effectively python/ipynb-only) stack
exactly — confirmed equivalent, not just avoided.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Finding: bumping mirrors-mypy v1.13.0 -> v2.4.0 surfaced 4 new
"object has no attribute" errors in test_rerun_inference.py. Root
cause isolated with a minimal repro: _failed_recipe() had no return
annotation, so mypy treats it as untyped and its call sites get type
Any; two tests then do `executed, run_root, kwargs = calls[0]` against
a `list[tuple[object, ...]]` capture list and `assert executed is
recipe` — mypy 2.4's sharper is-narrowing takes recipe's Any down to
executed's declared `object`, so the *next* line's `recipe.tuning_seed`
/ `.model_name` access errors. mypy 1.13 didn't narrow Any through
`is` this way.
Fix: annotate `_failed_recipe() -> Recipe` (Recipe imported under
TYPE_CHECKING to keep the existing local-import runtime pattern
unchanged) so recipe's static type is Recipe before the assert, not
Any. Confirmed in isolation (mypy_repro3.py) and against the real
file; tests/notebooks/test_rerun_inference.py still passes (33/33).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The new 3.14 leg failed in CI with "Failed to spawn: pytest". The
sync step passes --python 3.14, but the test step ran a bare `uv
run`, which falls back to .python-version (3.13), rebuilds the env
for 3.13 without the bench group, and so has no pytest. The 3.13
leg only passed because its versions happened to agree.

Set UV_PYTHON from the matrix at job level so every uv command uses
the same interpreter. Reproduced locally: a bare `uv run` after a
3.14 sync fails the same way, and with UV_PYTHON=3.14 it runs
pytest under 3.14.7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@junpenglao
junpenglao merged commit 1817f45 into main Oct 4, 2026
6 checks passed
@junpenglao
junpenglao deleted the ruff-py313-py314 branch October 4, 2026 15:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant