Upgrade ruff to 0.16.5, target Python 3.13, and prune dead noqa directives - #38625
Draft
antiguru wants to merge 5 commits into
Draft
Upgrade ruff to 0.16.5, target Python 3.13, and prune dead noqa directives#38625antiguru wants to merge 5 commits into
antiguru wants to merge 5 commits into
Conversation
…eter `bin/pyactivate` validates the interpreter running it and then hands virtualenv creation to `uv venv` without naming an interpreter. `uv` resolves one by its own preference order, which favors uv-managed installs over the system Python, so the virtualenv can end up on a different and older Python than the one the check just accepted. On a machine with system Python 3.14 and a uv-managed 3.10 present, the virtualenv is built on 3.10. Passing `sys.executable` removes that second, independent choice and makes the check authoritative. It also aligns the two creation paths, since the `venv.create` fallback right below already builds from the running interpreter. That fallback passes `clear=True`, and `uv venv` is now given `--clear` to match. Control reaches this branch only when the virtualenv is missing or its Python will not execute, which is exactly when it should be replaced. Without the flag `uv` refuses to touch an existing directory and aborts, so a virtualenv left behind without its `dep_stamp` wedges every later invocation instead of being rebuilt. The floor moves from 3.10 to 3.13. Python 3.10 has been security-only for years and reaches end of life in October 2026, and the tree had already drifted past it: two mzcompose files import `LiteralString` from `typing`, which needs 3.11. That went unnoticed because `ci/test/lint-main/checks/check-python-version.sh` only byte-compiles the tree, and compilation does not resolve imports, so a newer-than-floor import passes the check and fails at run time. 3.13 is what the CI builder already runs, since that is the system Python in Debian 13. Raising `target-version` lets ruff replace `datetime.timezone.utc` with the `datetime.UTC` alias and pull `LiteralString` back from `typing`, which is the whole of the mechanical churn here. Ruff trails at `py312` because the pinned version predates 3.13 and rejects `py313`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… bootstrap The version gate in `main` inspects the interpreter that runs the script, while the code the repository executes runs inside `misc/python/venv`. Those need not agree, because the virtualenv outlives the interpreter it was built from, and the reuse path only confirmed that a `dep_stamp` existed and that the virtualenv's Python could execute at all. Raising the minimum therefore left every existing virtualenv below it in place, and this branch is the first to depend on that difference, since `datetime.UTC` does not exist before 3.11. The liveness probe now also reports the version, so a virtualenv older than the minimum takes the existing recreation path. `uv venv --clear` refuses a target directory that is not a valid virtualenv and suggests `--force`. That refusal is an error rather than the `FileNotFoundError` the surrounding code catches, so it would abort with a traceback instead of falling back to `venv.create`. The half-finished directory this leaves behind is the case the comment above the check already describes, and `--force` is the flag that actually matches `clear=True`. The developer guide named 3.12 and derived it from the default Python in the most recent Ubuntu LTS. That release is now 26.04 "Resolute Raccoon", which ships 3.14, so the stated rule no longer produced the stated version and taken literally would demand a version newer than the one CI runs. The guide now gives the minimum directly and explains that it tracks the CI builder image, noting that a current LTS satisfies it. The minimum lives in `MIN_HEXVERSION` and `MIN_VERSION` so the gate, the probe, and the operator-facing messages cannot drift apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`uv venv --force` was added in uv 0.11.17. Earlier versions exit with a usage error, which reaches `subprocess.check_call` as a `CalledProcessError` and not as the `FileNotFoundError` that selects the `venv.create` fallback, so `bin/pyactivate` would abort. Everything in the tree runs through that script, and the CI builder image installs uv 0.9.10, so the flag cannot be used here. The version probe added alongside it makes the timing worse rather than academic. Any developer holding a virtualenv below the new minimum takes the recreation path on their first invocation, which is exactly the path that would have failed, and deleting the virtualenv by hand leads to the same call. Removing the directory before invoking `uv` behaves the same on every version and needs no flag. It also settles what `--clear` and `--force` disagree about, a path that exists but is not a virtualenv, which is the half-finished state the comment above the check already describes. The `venv.create` fallback is unaffected, since it creates a directory that does not exist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pinned ruff was 0.0.292, released in October 2023, and it rejects `py313`, so the lint target trailed the minimum version the repository enforces. This upgrade closes that gap and lets the `UP` rules see the language level we actually run on. Two interface changes come with it. Linting now requires the `check` subcommand, since a bare `ruff <files>` is no longer accepted, which affects `bin/fmt` and the lint check script. Top-level linter settings moved under a `lint` section, so `select` and `isort` become `lint.select` and `lint.isort`. Retargeting to 3.13 surfaces rewrites the old target could not suggest. The mechanical ones are PEP 695 type parameters in place of `TypeVar`, dropping default type arguments, and a few string and annotation modernizations. The `TypeVar` bindings that the PEP 695 conversion left with no remaining references are removed, since ruff rewrites the signature but does not clean up the declaration. The two `mzexplore` enums become `StrEnum`. For `ExplainStage` nothing changes at all, because it already defined `__str__` to return its value, and that override is now redundant. For `ItemType`, `str()` and formatting now produce the value rather than `ItemType.NAME`. No caller observes that: every use goes through `sql()`, which reads the value either way, and formatting the member directly does not appear anywhere. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Of 38 `noqa` directives, 29 had no effect. They were invisible because the rule that reports them was not selected, and they predate the ruff upgrade: version 0.0.292 reports the same 29. Most are star imports annotated `F401 F403`. A star import triggers F403 and never F401, so only the F403 half ever did anything, and those directives are narrowed to it. Three name rules from families this repository does not select, `SLF001`, `E731` and `BLE001`, are removed along with one blanket directive on an import that is used. The eight that remain sit on genuine re-exports in `mzexplore/__init__.py`, where F401 does fire. Selecting RUF100 keeps the set honest, since a directive that stops applying now fails the lint rather than accumulating. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
antiguru
force-pushed
the
ruff-upgrade
branch
from
September 2, 2026 13:32
c6952bc to
383e577
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #38624, which must land first. This branch is built on it, so until that merges the diff here also shows its commits. Review the two commits on this branch; the 3.13 target depends on the floor that #38624 raises.
The pinned ruff was
0.0.292, released in October 2023, and it rejectspy313, so the lint target trailed the minimum version the repository enforces. This upgrade closes that gap and lets theUPrules see the language level we actually run on.Interface changes in ruff
Linting now requires the
checksubcommand. A bareruff <files>fails witherror: unrecognized subcommand, which would have brokenbin/fmtandcheck-python-files.sh, so both call sites are updated. Top-level linter settings also moved under alintsection, makingselectandisortintolint.selectandlint.isort.What retargeting surfaced
24 findings, all addressed. Most are mechanical: PEP 695 type parameters in place of
TypeVar, dropping default type arguments such asGenerator[int, None, None], and a few string and annotation modernizations. Ruff rewrites a generic signature but does not remove the now-unreferencedTypeVardeclaration, so those are cleaned up by hand inutil.py,buildkite.py,spawn.py, andbenchmark_result_evaluator.py.The two
mzexploreenums becomeStrEnum, which is the only finding with observable semantics, so it was checked member by member rather than trusted:ExplainStageis unchanged in every respect, because it already defined__str__to return its value. That override is now redundant and removed.ItemTypechanges:str()and formatting produce the value rather thanItemType.NAME. No caller observes this. Every use goes throughsql(), which calls astrmethod on the value either way, and no code formats a member directly.That was verified by snapshotting
str, formatting,value, equality,isinstance(str), by-value lookup, andsql()/show_create()for all sixteen members before and after the conversion, and diffing. The only differences are theItemTypestr/format entries described above.Dead suppressions
The second commit selects
RUF100and removes what it finds. Of 38noqadirectives, 29 suppressed nothing. They were invisible because the rule that reports them was not selected, and they are not fallout from this upgrade: ruff 0.0.292 reports the same 29.Most are star imports annotated
F401 F403. A star import triggers F403 and never F401, so only the F403 half ever did anything, and those are narrowed to it. Three directives name rules from families this repository does not select,SLF001,E731andBLE001, and one is a blanket directive on an import that is used; those four go. The eight that remain sit on genuine re-exports inmzexplore/__init__.py, where F401 does fire. Selecting the rule means a directive that stops applying fails the lint instead of accumulating.Verification
bin/lintpasses in full. That includes pyright with--warningsover the whole Python tree, which is what actually validates the PEP 695 rewrites, plus both updatedruff checkinvocations andcheck-python-version.sh.Not addressed here
Beyond
RUF100nothing selects new rule families;selectremainsF,I,UP,E711. A three-year upgrade will have changed behavior within those families beyond what our tree currently trips, so a later divergence is possible even though the tree is clean today. The dbt adapter keeps itspy38target, which ruff 0.16.5 still accepts, sincepyproject.tomlexempts it from internal compatibility rules.Release notes
No user-visible changes.
🤖 Posted by Claude Code