Unbreak the linter job: pin flake8-isort/isort, and sync the mypy hook's stub pins - #2281
Conversation
…urfaces .pre-commit-config.yaml's mypy hook documents that its `==`-pinned stub dependencies MUST match requirements/base.txt + requirements/local.txt, because the hook is what CI actually type-checks with. They had drifted (django-stubs 6.0.6 vs 6.0.7, djangorestframework-stubs 3.17.0 vs 3.17.1), and the pending Dependabot bumps #2265 (django-stubs 6.1.0) and #2260 (djangorestframework-stubs 3.18.0) widen the gap further. Bumping the hook to those versions is not a no-op: django-stubs 6.1.0 surfaces 7 previously-invisible type errors. All 7 are typing-only; no runtime behavior changes. QuerySets.py x6 - six guardian-permission id lists are built as a lazy `values_list` queryset inside a `try` and fall back to `[]` in the matching `except LookupError`. 6.1.0 types `values_list(..., flat=True)` as `QuerySet[Model, int]`, which no longer unifies with `list[Never]`. Each variable now carries an explicit `Iterable[Any]` declaration; they are only ever consumed by an `__in` lookup, so that is the accurate common type. test_corpus_canonical_caml_migration.py x1 - the test asserts `apps.get_model("corpuses", "CorpusDescriptionRevision")` raises LookupError, i.e. the lazy reference is unresolvable on purpose. 6.1.0's plugin resolves get_model() string pairs statically and errors on a miss, so this is a false positive; silenced narrowly with `# type: ignore[misc]`. Verified with `pre-commit run --all-files`: mypy passes with the bumped stubs (it fails with 7 errors without these fixes). black/isort/pyupgrade/yaml/ changelog hooks all pass. flake8 reports 10 pre-existing I001/I005 findings in pydantic_ai_agents.py and compact_pawls.py that reproduce identically on a clean origin/main checkout and are untouched by this change.
The linter job is failing on main and on every open PR with 10 I001/I005
findings in two files nobody edited (llms/agents/pydantic_ai_agents.py,
utils/compact_pawls.py). No commit caused it.
The flake8 hook declared `additional_dependencies: [flake8-isort]` with no
version. pre-commit re-resolves additional_dependencies from scratch whenever it
rebuilds a hook env, so flake8-isort — which re-implements the isort check
inside flake8 using whatever isort it resolves — drifted to isort 9.0.1, while
the standalone `isort` hook stayed on the 6.0.1 its `rev` pins. isort 9 and
isort 6 disagree about repeated `from X import (...)` statements, which is
exactly the shape isort 6 emits for aliased imports. So the isort hook wrote a
layout the flake8 hook rejected, on an unchanged tree.
The mypy hook right below already documents this exact failure mode ('anything
less than == can drift'); the flake8 hook just never followed it. Both entries
are now ==-pinned, with isort tracking the isort hook's rev.
Verified: `pre-commit run --all-files` is fully green on this branch, and
`pre-commit run flake8 --all-files` reproduces all 10 findings on a clean
origin/main checkout before the pin and passes after it.
ReviewThis is a well-scoped, well-documented CI/tooling fix. The root-cause analysis for both issues (unpinned
One thing worth flagging (not a blocker)The mypy hook's own comment (and this PR's stated rationale) is that the This PR bumps the hook to The PR description already discloses this ("the end state is only self-consistent once all three land"), and given trunk-based development with same-day merges this is probably an acceptable, deliberate tradeoff to unblock the other three PRs. But it might be worth a one-line note in the mypy hook's comment (or in the merge order) making explicit that there's a brief window where the pin is intentionally ahead of Minor
TestingNo backend/frontend test suite is needed here since the change is CI-tooling + typing-only fixes; the PR's stated verification ( Overall: solid, low-risk fix for a real CI-breaking issue. No security concerns (pure tooling/typing), no functional/runtime behavior changes. |
The linter job on this PR fails with 10 I001/I005 findings in opencontractserver/llms/agents/pydantic_ai_agents.py and opencontractserver/utils/compact_pawls.py — files this PR does not touch. The failure reproduces identically on a clean origin/main checkout: the flake8 hook's unpinned flake8-isort floated to isort 9.0.1 while the standalone isort hook stayed rev-pinned to 6.0.1, and the two disagree about repeated `from X import (...)` statements. Cherry-picking the pin here so this PR can reach green without waiting on #2281 to merge. It no-ops once main carries the same change.
The linter job on this PR fails with 10 I001/I005 findings in opencontractserver/llms/agents/pydantic_ai_agents.py and opencontractserver/utils/compact_pawls.py — files this PR does not touch. The failure reproduces identically on a clean origin/main checkout: the flake8 hook's unpinned flake8-isort floated to isort 9.0.1 while the standalone isort hook stayed rev-pinned to 6.0.1, and the two disagree about repeated `from X import (...)` statements. Only the flake8 pin is ported. #2281 also bumps this hook file's mypy stub pins, but that half needs the type fixes that ship with it, so it stays there. Cherry-picking so this PR can reach green without waiting on #2281 to merge. It no-ops once main carries the same change.
The linter job on this PR fails with 10 I001/I005 findings in opencontractserver/llms/agents/pydantic_ai_agents.py and opencontractserver/utils/compact_pawls.py — files this PR does not touch. The failure reproduces identically on a clean origin/main checkout: the flake8 hook's unpinned flake8-isort floated to isort 9.0.1 while the standalone isort hook stayed rev-pinned to 6.0.1, and the two disagree about repeated `from X import (...)` statements. Cherry-picking the pin here so this PR can reach green without waiting on #2281 to merge. It no-ops once main carries the same change.
The linter job on this PR fails with 10 I001/I005 findings in opencontractserver/llms/agents/pydantic_ai_agents.py and opencontractserver/utils/compact_pawls.py — files this PR does not touch. The failure reproduces identically on a clean origin/main checkout: the flake8 hook's unpinned flake8-isort floated to isort 9.0.1 while the standalone isort hook stayed rev-pinned to 6.0.1, and the two disagree about repeated `from X import (...)` statements. Only the flake8 pin is ported. #2281 also bumps this hook file's mypy stub pins, but that half needs the type fixes that ship with it, so it stays there. Cherry-picking so this PR can reach green without waiting on #2281 to merge. It no-ops once main carries the same change.
The linter job on this PR fails with 10 I001/I005 findings in opencontractserver/llms/agents/pydantic_ai_agents.py and opencontractserver/utils/compact_pawls.py — files this PR does not touch. The failure reproduces identically on a clean origin/main checkout: the flake8 hook's unpinned flake8-isort floated to isort 9.0.1 while the standalone isort hook stayed rev-pinned to 6.0.1, and the two disagree about repeated `from X import (...)` statements. Cherry-picking the pin here so this PR can reach green without waiting on #2281 to merge. It no-ops once main carries the same change.
The linter job on this PR fails with 10 I001/I005 findings in opencontractserver/llms/agents/pydantic_ai_agents.py and opencontractserver/utils/compact_pawls.py — files this PR does not touch. The failure reproduces identically on a clean origin/main checkout: the flake8 hook's unpinned flake8-isort floated to isort 9.0.1 while the standalone isort hook stayed rev-pinned to 6.0.1, and the two disagree about repeated `from X import (...)` statements. Only the flake8 pin is ported. #2281 also bumps this hook file's mypy stub pins, but that half needs the type fixes that ship with it, so it stays there. Cherry-picking so this PR can reach green without waiting on #2281 to merge. It no-ops once main carries the same change.
The linter job on this PR fails with 10 I001/I005 findings in opencontractserver/llms/agents/pydantic_ai_agents.py and opencontractserver/utils/compact_pawls.py — files this PR does not touch. The failure reproduces identically on a clean origin/main checkout: the flake8 hook's unpinned flake8-isort floated to isort 9.0.1 while the standalone isort hook stayed rev-pinned to 6.0.1, and the two disagree about repeated `from X import (...)` statements. Cherry-picking the pin here so this PR can reach green without waiting on #2281 to merge. It no-ops once main carries the same change.
The linter job on this PR fails with 10 I001/I005 findings in opencontractserver/llms/agents/pydantic_ai_agents.py and opencontractserver/utils/compact_pawls.py — files this PR does not touch. The failure reproduces identically on a clean origin/main checkout: the flake8 hook's unpinned flake8-isort floated to isort 9.0.1 while the standalone isort hook stayed rev-pinned to 6.0.1, and the two disagree about repeated `from X import (...)` statements. Only the flake8 pin is ported. #2281 also bumps this hook file's mypy stub pins, but that half needs the type fixes that ship with it, so it stays there. Cherry-picking so this PR can reach green without waiting on #2281 to merge. It no-ops once main carries the same change.
CI (Python 3.12) reported the `# type: ignore[misc]` on test_no_corpus_description_revision_model as an unused ignore, while a cold-cache run on 3.11 needs it — mypy.ini sets warn_unused_ignores, so the comment cannot be right in both. The plugin only resolves *literal* get_model() string pairs, so the model name now lives in a str-annotated local instead. Neither interpreter's plugin has a literal to match, and the test's runtime behavior and assertion are untouched. Verified with a cleared .mypy_cache: `pre-commit run --all-files` is fully green.
|
Reviewed the diff ( Overall: this is a tight, well-scoped tooling fix. The root-cause analysis in the description checks out against the diff, and the fix pattern for the Correctness
Minor
Test coverage: No new tests added, but none are needed — this is a CI/type-checking config fix plus type-annotation-only production code changes and a test-only fix for a stub-resolution false positive. The existing test suite plus Security: N/A — no security-relevant surface touched. Nice work isolating a flaky-looking CI failure ("red on main with no commit responsible") down to the actual mechanism (unpinned transitive |
backend.yml's linter job runs mypy TWICE: the pinned pre-commit hook, and then a second authoritative `python -m mypy --config-file mypy.ini` (backend.yml:107) against whatever requirements/local.txt installs. So bumping django-stubs there does gate CI, and 6.1.0 surfaces 7 errors this PR would otherwise land red with: QuerySets.py x6 - six guardian-permission id lists are a lazy `values_list` queryset in a `try` and `[]` in the matching `except LookupError`. 6.1.0 types `values_list(..., flat=True)` as `QuerySet[Model, int]`, which no longer unifies with `list[Never]`. Each now carries an explicit `Iterable[Any]` declaration — they are only ever consumed by an `__in` lookup. test_corpus_canonical_caml_migration.py x1 - the test asserts the model is GONE, so the lazy reference is unresolvable on purpose; 6.1.0's plugin resolves literal get_model() string pairs statically and errors on a miss. The name moves into a str-annotated local so there is no literal to match. Whether the error fires varies by interpreter and mypy version, which is why a `# type: ignore` is not used here. Typing-only; no runtime behavior changes. No-ops once main carries #2281.
backend.yml's linter job runs mypy TWICE: the pinned pre-commit hook, and then a second authoritative `python -m mypy --config-file mypy.ini` (backend.yml:107) against whatever requirements/local.txt installs. So bumping django-stubs there does gate CI, and 6.1.0 surfaces 7 errors this PR would otherwise land red with: QuerySets.py x6 - six guardian-permission id lists are a lazy `values_list` queryset in a `try` and `[]` in the matching `except LookupError`. 6.1.0 types `values_list(..., flat=True)` as `QuerySet[Model, int]`, which no longer unifies with `list[Never]`. Each now carries an explicit `Iterable[Any]` declaration — they are only ever consumed by an `__in` lookup. test_corpus_canonical_caml_migration.py x1 - the test asserts the model is GONE, so the lazy reference is unresolvable on purpose; 6.1.0's plugin resolves literal get_model() string pairs statically and errors on a miss. The name moves into a str-annotated local so there is no literal to match. Whether the error fires varies by interpreter and mypy version, which is why a `# type: ignore` is not used here. Typing-only; no runtime behavior changes. No-ops once main carries #2281.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Summary
Two
.pre-commit-config.yamlpin problems found while getting #2257, #2260, #2264 and #2265 to green. The first is breakingmainright now; the second is a trap the pending stub bumps walk into.1. The
linterjob is red onmainand on every open PR, with no commit responsible.It fails with 10
I001/I005findings inopencontractserver/llms/agents/pydantic_ai_agents.pyandopencontractserver/utils/compact_pawls.py— files none of those PRs touch. The failure reproduces identically on a cleanorigin/maincheckout.Cause: the flake8 hook declared
additional_dependencies: [flake8-isort]with no version. pre-commit re-resolvesadditional_dependenciesfrom scratch every time it rebuilds a hook env, soflake8-isort— which re-implements the isort check inside flake8 using whatever isort it resolves — drifted up to isort 9.0.1, while the standaloneisorthook stayed on the 6.0.1 itsrevpins. isort 9 and isort 6 disagree about repeatedfrom X import (...)statements, which is exactly the shape isort 6 emits for aliased imports. So theisorthook kept writing a layout theflake8hook rejected. Nothing in the tree had to change for CI to go red.The mypy hook immediately below already documents this exact failure mode — "pre-commit autoupdate only bumps
rev; additional_dependencies are resolved fresh every time the hook env is (re)built, so anything less than==can drift" — the flake8 hook just never followed it.2. The mypy hook's stub pins had drifted from
requirements/local.txt, and closing the gap is not a no-op.That hook's own comment says its
==-pinned stubs MUST matchrequirements/base.txt+requirements/local.txt. They didn't (django-stubs6.0.6 vs 6.0.7,djangorestframework-stubs3.17.0 vs 3.17.1), and #2265 / #2260 widen the gap to 6.1.0 / 3.18.0. Because CI type-checks with the hook's stubs, those bumps land green while leaving the dev/test image type-checking against something else.Syncing the pins surfaces 7 previously-invisible errors from
django-stubs6.1.0 — the "Explicit through default manager" change the reviewer on #2265 flagged as worth checking. All 7 are typing-only; no runtime behavior changes.Changes
.pre-commit-config.yaml— flake8 hook:additional_dependenciesnow==-pinsflake8-isort==7.0.0andisort==6.0.1, with a comment notingisorthere must track theisorthook'srev..pre-commit-config.yaml— mypy hook:django-stubs6.0.6 → 6.1.0,djangorestframework-stubs3.17.0 → 3.18.0.opencontractserver/shared/QuerySets.py(6 errors) — six guardian-permission id lists are built as a lazyvalues_listqueryset inside atryand fall back to[]in the matchingexcept LookupError. 6.1.0 typesvalues_list(..., flat=True)asQuerySet[Model, int], which no longer unifies withlist[Never]. Each variable now carries an explicitIterable[Any]declaration — they are only ever consumed by an__inlookup, so that is the accurate common type.opencontractserver/tests/test_corpus_canonical_caml_migration.py:161(1 error) — the test assertsapps.get_model("corpuses", "CorpusDescriptionRevision")raisesLookupError, i.e. the lazy reference is unresolvable on purpose. 6.1.0's plugin resolvesget_model()string pairs statically and errors on a miss, so this is a false positive. Silenced narrowly with# type: ignore[misc]plus a comment; the test's logic and assertions are unchanged.changelog.d/flake8-isort-pin.fixed.md,changelog.d/mypy-hook-stub-parity.changed.md.Test plan
flake8andmypy.pre-commit run flake8 --all-fileson a cleanorigin/maincheckout reproduces all 10I001/I005findings before the pin, and passes after it.isort 9.0.1, the isort hook env6.0.1.Relationship to the open dependency bumps
Ordering does not matter for CI, but the end state is only self-consistent once all three land:
requirements/local.txttodjango-stubs==6.1.0; this PR brings the hook to the same version.requirements/local.txttodjangorestframework-stubs==3.18.0(plusdjango-stubs==6.1.0, which 3.18.0 requires); this PR brings the hook to the same.maincarries it.Checklist
pre-commit run --all-filespasses (black, isort, flake8, mypy)changelog.d/requirements/local.txt🤖 Generated with Claude Code
https://claude.ai/code/session_01Q3mE12T4oCZNyorWYJKF5R
Generated by Claude Code