Skip to content

Enforce mypy in CI behind a ratchet - #757

Merged
wbarnha merged 1 commit into
masterfrom
claude/mypy-ratchet-ci
Aug 5, 2026
Merged

Enforce mypy in CI behind a ratchet#757
wbarnha merged 1 commit into
masterfrom
claude/mypy-ratchet-ci

Conversation

@wbarnha

@wbarnha wbarnha commented Aug 5, 2026

Copy link
Copy Markdown
Member

Description

Follow-up to #756, which slipped through because nothing in CI type-checks.

  • The lint job runs scripts/check and nothing else, and the mypy line in that script has been commented out since the file was added in df20a17 — it never ran.
  • tox -e typecheck and make typecheck do invoke mypy -p faust, but no workflow invokes tox or make, so they only fire if a contributor runs them by hand.
  • [tool.mypy] in pyproject.toml has been configured the whole time with nothing reading it in anger.
  • flake8 cannot cover for it: pycodestyle and pyflakes do no type inference, so implicit-Optional defaults and missing annotations are invisible to the checks that do run.

mypy -p faust reports 692 errors across 78 modules, so it can't simply be switched on. This adds a ratchet instead.

How it works

[[tool.mypy.overrides]] with ignore_errors = true silences exactly the 78 modules that still have errors. The other 86 are enforced — as is every module added to the package from here on, since anything not on the list is checked by default. The list is documented to only ever shrink: cleaning up a module means deleting its line in the same PR, so it can't regress.

Using mypy's own per-module config rather than a shell allowlist or grep-filtered output means mypy -p faust just exits zero, and tox -e typecheck / make typecheck become useful again for free.

Two things had to be pinned first

Without these the check is red on one machine and green on another:

  • python_version was unset, so mypy targeted whichever interpreter ran it. Pinned to 3.12, the lint job's interpreter. Typeshed differs enough between versions to change the verdict — faust/app/_attached.py is clean targeting 3.10 but reports asyncio.wait receiving a bare Awaitable from 3.11 on, which is a real hazard given the CI matrix runs through 3.14.
  • mypy>=0.750 floats to whatever is newest, and the enforced set moves with it. 1.19.1 and 2.3.0 disagree about 6 modules, largely because 2.x reads inline types from py.typed packages that 1.x ignored. Pinned to 2.3.0 for the same reason test.txt already pins the formatters, and typecheck.txt is now pulled into test.txt so the lint job installs it.

Verification

Against merged master (aeec5f3):

  • scripts/check exits 0 with confluent-kafka installed and without it — the lint job doesn't install that extra, and the silenced set is identical either way, so the check can't pass locally and fail in CI over an optional dependency.
  • The list is exact: 78 listed, 78 dirty, no module dirty-but-unlisted (which would fail CI) and none listed-but-already-clean (which would be dead weight).
  • Appending an unannotated def _ratchet_probe(x): return x to faust/utils/urls.py (an enforced module) fails the check with error: Function is missing a type annotation, exit 1. Reverted after testing.
  • No source files are touched by this PR — only pyproject.toml, scripts/check, and two requirements files.

Note on scope

faust/transport/drivers/confluent.py is on the silenced list despite #756. Under the pinned mypy that PR takes it from 53 errors to 17, not to zero: annotating AsyncConsumer.__init__ lets mypy resolve self.consumer to the real confluent_kafka.Consumer instead of Any, which surfaces pre-existing bugs (AsyncConsumer has no seek or seek_to_beginning; sync callables passed to call_thread, which wants awaitables). Those are real defects worth their own PR, and removing that line from the ratchet list is the natural way to close it out.

Nothing in CI type-checks today.  The lint job runs `scripts/check` and
nothing else, and the mypy line in that script has been commented out
since the file was added in df20a17 -- it never ran.  `tox -e typecheck`
and `make typecheck` do invoke `mypy -p faust`, but no workflow invokes
tox or make, so they only fire if a contributor runs them by hand.  The
`[tool.mypy]` block in pyproject.toml has been configured the whole time
with nothing reading it in anger.

flake8 cannot cover for it: pycodestyle and pyflakes do no type
inference, so implicit-Optional defaults and missing annotations are
invisible to the checks that do run.

`mypy -p faust` reports 692 errors across 78 modules, so it cannot
simply be switched on.  Instead, silence exactly those 78 modules with
`ignore_errors` and enforce the other 86.  Everything outside the list
is checked, including every module added from here on, and the list is
documented to only ever shrink.

Two things had to be nailed down first, or the check would be red on one
machine and green on another:

* `python_version` was unset, so mypy targeted whichever interpreter ran
  it.  Pin it to 3.12, the lint job's interpreter -- typeshed differs
  enough between versions to change the verdict (`asyncio.wait` in
  faust/app/_attached.py is clean at 3.10 and an error from 3.11 on).
* `mypy>=0.750` floats to whatever is newest, and the enforced set moves
  with it -- 1.19.1 and 2.3.0 disagree about 6 modules, largely because
  2.x reads inline types from py.typed packages that 1.x ignored.  Pin
  it, for the reason test.txt already pins the formatters, and pull
  typecheck.txt into test.txt so the lint job installs it.

Verified that `scripts/check` exits zero both with and without
confluent-kafka installed -- the lint job does not install that extra,
and the silenced set is identical either way -- and that adding an
unannotated function to an enforced module fails the check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012TbXZ1ATm7RvvDsqgZ3Xy6
@wbarnha
wbarnha force-pushed the claude/mypy-ratchet-ci branch from 58c506a to ad3091c Compare August 5, 2026 19:19
@wbarnha
wbarnha changed the base branch from claude/confluent-type-annotations-qd2mop to master August 5, 2026 19:19
@wbarnha wbarnha closed this Aug 5, 2026
@wbarnha wbarnha reopened this Aug 5, 2026
@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.06%. Comparing base (aeec5f3) to head (ad3091c).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #757   +/-   ##
=======================================
  Coverage   96.06%   96.06%           
=======================================
  Files         103      103           
  Lines       11072    11072           
  Branches     1191     1191           
=======================================
  Hits        10636    10636           
  Misses        345      345           
  Partials       91       91           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

wbarnha commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

Note on the red codecov/project check — I don't think it needs action, and it isn't caused by this PR.

This PR changes no source files (pyproject.toml, scripts/check, and two requirements files only), and codecov agrees: Files 103 → 103, Lines 11072 → 11072, Branches 1191 → 1191, "all modified and coverable lines are covered by tests." The delta is 2 lines out of 11,072.

Project coverage oscillates in a narrow band across commits on master itself:

commit codecov/project
cdef39a success — 96.04% (+0.07%)
13eb70e success — 95.98% (+0.00%)

This PR reports 96.04% — the same figure master recorded at cdef39a. Base aeec5f3 happened to be measured at 96.06%, the top of that band, so an ordinary sample lower in it registers as a decrease.

Two things make that fail rather than pass:

  1. There is no codecov.yml in the repo, so the project status uses codecov's defaults — target: auto, threshold: 0%. Any decrease at all, including jitter, is a failure. A threshold of even 0.5% would absorb this.
  2. The earlier 32.55% reading on this same commit was codecov scoring an incomplete upload set (its own comment: "HEAD has 11 uploads less than BASE", 16 vs 5). It re-reported at 96.04% once more legs finished, and a couple of legs were still uploading even then.

Worth noting this check is advisory here: the branch-protection gate is the ✅ Ensure the required checks passing job, which is needs: [lint, test-pytest]codecov/project is not among its dependencies.

Separately, the result this PR was really testing: Check linting passed, which is mypy -p faust running in CI for the first time. The ratchet list holds on Python 3.12 in an environment without confluent-kafka installed.

If the coverage threshold is worth tightening up, adding a codecov.yml with a small threshold would stop this recurring on unrelated PRs — happy to open that separately, but it's out of scope here.


Generated by Claude Code

wbarnha commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

Correcting my comment above: all checks are now green, and my explanation for the red codecov/project was wrong.

Once the last two legs uploaded, codecov re-reported 96.06% against base 96.06%+0.00%, with Hits (10636), Misses (345), and Partials (91) all exactly equal to base. codecov/project and codecov/patch both flipped to success; 30 checks, 0 non-success.

So the whole thing was the incomplete upload set from start to finish — first 5 of 16 uploads (32.55%), then 14 of 16 (-0.02%), then all 16 (+0.00%). There was never any coverage delta.

My "run-to-run jitter" reasoning was a bad inference: I cited 96.04% and 95.98% from earlier master commits, but those were each measured against their own parent, so they say nothing about variance at a fixed commit. Please disregard that part, and the suggestion that a codecov.yml threshold is needed — nothing here demonstrates it is.

What stands from the earlier comment: this PR changes no source files, and Check linting passed, which is mypy -p faust running in CI for the first time with the ratchet list holding on Python 3.12 without confluent-kafka installed.


Generated by Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Ready to approve

The changes consistently wire mypy into CI with deterministic pins and a clear per-module ratchet, without altering runtime code paths.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

This PR wires mypy into the existing CI lint job via scripts/check, but “behind a ratchet” so only already-clean modules are enforced while known-dirty modules are silenced via per-module mypy overrides. This makes type-checking effective immediately without requiring a large, blocking cleanup of the current backlog of typing errors.

Changes:

  • Enable mypy -p faust in scripts/check (CI lint job), relying on per-module ignore_errors overrides as a ratchet.
  • Pin mypy to a specific version to keep CI verdicts stable over time.
  • Pin mypy’s target python_version to avoid interpreter-dependent results.
File summaries
File Description
scripts/check Runs mypy -p faust as part of the lint script, enforcing typing for modules not in the ratchet ignore list.
requirements/typecheck.txt Pins mypy to a specific version to prevent CI drift as mypy evolves.
requirements/test.txt Ensures CI installs the pinned mypy by including typecheck.txt in the lint/test requirements set.
pyproject.toml Adds python_version pin and the per-module ignore_errors ratchet list under [[tool.mypy.overrides]].
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 0
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

@wbarnha
wbarnha merged commit 4af976b into master Aug 5, 2026
31 checks passed
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.

3 participants