Skip to content

Make mypy pass and enforce type checks in CI - #83

Merged
wbarnha merged 7 commits into
masterfrom
claude/mode-issue-4-l89d5h
Aug 5, 2026
Merged

Make mypy pass and enforce type checks in CI#83
wbarnha merged 7 commits into
masterfrom
claude/mode-issue-4-l89d5h

Conversation

@wbarnha

@wbarnha wbarnha commented Aug 5, 2026

Copy link
Copy Markdown
Member

Closes #4. mypy -p mode reported 127 errors under a current mypy; the
type checks had been commented out of scripts/lint.sh, so nothing caught
the drift. This fixes every error and wires type checking back into CI.

Fixes grouped by cause:

  • singledispatch fallbacks (want_seconds, rate, level_number,
    level_name) annotated their first parameter with only the fallback's
    own type, so registering str/timedelta was rejected and every
    caller passing Seconds/Severity failed. They now name the full
    union they dispatch over.
  • Proxy roles in mode/locals.py were written against older typeshed
    signatures: Coroutine.throw/AsyncGenerator.athrow are overloaded,
    Mapping.items()/keys() return views, and MutableSet/
    MutableSequence in-place operators return Self. The two throw
    errors quoted in the issue are among these.
  • mode/utils/tracebacks.py read gi_frame/cr_frame/ag_frame/
    ag_await/gi_yieldfrom/cr_await off the abstract protocols, which
    do not declare them; it now casts to the concrete types.*Type. Those
    attributes are Optional, so the frame getters say so.
  • Heap is bound to SupportsRichComparison: heapq orders elements by
    comparing them.
  • FlowControlQueue/ThrowableQueue used an unbound type variable in
    their signatures; they are now generic, so ThrowableQueue[int] means
    what it says.
  • ServiceT.beacon is abstract like every other property on the type.
    ServiceBase._format_log already requires it; Service and
    ServiceProxy both implement it.
  • FileLogProxy matches TextIO: line_buffering is a property,
    read/readline/readlines/write are str, not AnyStr.
  • Dropped the Python 3.6-era asyncio.Task.all_tasks/current_task
    fallbacks and 13 stale # type: ignore comments.
  • Service._actually_start/itertimer read should_stop into a local
    before each check. mypy folds repeated reads of a property into the
    first result and then calls the later checks dead code.

Type checking now runs as its own CI job via scripts/typecheck.sh: mypy
needs CPython 3.10+ and cannot run under PyPy, so it cannot go on every
leg of the test matrix. The mypy floor moves to 2.0.0, the first release
whose bundled typeshed the package checks clean against.

Also silences a new RUF063 from a recent ruff by spelling the annotation
lookup vars(cls); that failure predates this change.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_017q5Vvxifyg39rFZ6x2YUA7

claude added 7 commits August 5, 2026 19:05
Closes #4. `mypy -p mode` reported 127 errors under a current mypy; the
type checks had been commented out of scripts/lint.sh, so nothing caught
the drift. This fixes every error and wires type checking back into CI.

Fixes grouped by cause:

- singledispatch fallbacks (`want_seconds`, `rate`, `level_number`,
  `level_name`) annotated their first parameter with only the fallback's
  own type, so registering `str`/`timedelta` was rejected and every
  caller passing `Seconds`/`Severity` failed. They now name the full
  union they dispatch over.
- Proxy roles in `mode/locals.py` were written against older typeshed
  signatures: `Coroutine.throw`/`AsyncGenerator.athrow` are overloaded,
  `Mapping.items()`/`keys()` return views, and `MutableSet`/
  `MutableSequence` in-place operators return `Self`. The two `throw`
  errors quoted in the issue are among these.
- `mode/utils/tracebacks.py` read `gi_frame`/`cr_frame`/`ag_frame`/
  `ag_await`/`gi_yieldfrom`/`cr_await` off the abstract protocols, which
  do not declare them; it now casts to the concrete `types.*Type`. Those
  attributes are `Optional`, so the frame getters say so.
- `Heap` is bound to `SupportsRichComparison`: `heapq` orders elements by
  comparing them.
- `FlowControlQueue`/`ThrowableQueue` used an unbound type variable in
  their signatures; they are now generic, so `ThrowableQueue[int]` means
  what it says.
- `ServiceT.beacon` is abstract like every other property on the type.
  `ServiceBase._format_log` already requires it; `Service` and
  `ServiceProxy` both implement it.
- `FileLogProxy` matches `TextIO`: `line_buffering` is a property,
  `read`/`readline`/`readlines`/`write` are `str`, not `AnyStr`.
- Dropped the Python 3.6-era `asyncio.Task.all_tasks`/`current_task`
  fallbacks and 13 stale `# type: ignore` comments.
- `Service._actually_start`/`itertimer` read `should_stop` into a local
  before each check. mypy folds repeated reads of a property into the
  first result and then calls the later checks dead code.

Type checking now runs as its own CI job via scripts/typecheck.sh: mypy
needs CPython 3.10+ and cannot run under PyPy, so it cannot go on every
leg of the test matrix. The mypy floor moves to 2.0.0, the first release
whose bundled typeshed the package checks clean against.

Also silences a new RUF063 from a recent ruff by spelling the annotation
lookup `vars(cls)`; that failure predates this change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017q5Vvxifyg39rFZ6x2YUA7
Dropping them cost call-site checking on `proxy.update(...)` for no good
reason: the base's argument types live in `_typeshed`, which is not
importable at runtime but is fine under `TYPE_CHECKING`.

Three of typeshed's five overloads carry a `self: SupportsGetItem[str, _VT]`
annotation restricting `**kwargs` to str-keyed mappings; no single
implementation signature satisfies those, so the kwargs overload stays
unrestricted, exactly as it was before. The other two now match the base
instead of narrowing `SupportsKeysAndGetItem` to `Mapping`, which is what
made them fail in the first place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017q5Vvxifyg39rFZ6x2YUA7
`_HT` said nothing about what it constrains. The new name states it:
elements must support `<`/`>`, because heapq orders them by comparing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017q5Vvxifyg39rFZ6x2YUA7
`mode.utils.aiter`'s module docstring still claimed aiter and anext were
"missing" methods -- they have been builtins since Python 3.10. Replace
it with what actually justifies keeping these: mode's `aiter` dispatches
on synchronous iterables too, `anext` takes `*default`, and both shadow
the builtins for the rest of the module. Same note on each function.

`Heap` now states that its elements must be orderable and why, so the
`_ComparableT` bound is explained where users meet it rather than only at
the TypeVar.

Both files are published via docs/references (mkdocstrings), so this
lands on the docs site.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017q5Vvxifyg39rFZ6x2YUA7
requires-python goes to >=3.10, along with the 3.9 classifier, ruff's
target-version, the 3.9 and pypy3.9 CI legs, and the conditional
importlib-metadata dependency.

That makes three version branches dead, so they go too:

- objects.py carried a backport of `inspect.get_annotations` for 3.9;
  it now imports the stdlib function directly.
- `UNION_TYPES` no longer has to omit `types.UnionType`.
- `load_extension_class_names` no longer probes for `.select`;
  `entry_points()` has had it since 3.10.

The mypy requirement loses its `python_version >= "3.10"` marker, which
only existed because mypy could not be installed on the 3.9 leg. The
PyPy exclusion stays: mypy still cannot run under PyPy, and that alone
is why type checking is a separate job.

Raising ruff's target-version surfaces ~350 pyupgrade findings asking
for `X | Y` / `X | None` annotations and `collections.abc` imports
across the package. Those rules are ignored for now rather than
answered here: it is a mechanical migration that deserves its own diff,
and some of the aliases involved are evaluated at runtime.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017q5Vvxifyg39rFZ6x2YUA7
mypy hard-exits on PyPy -- `mypy/main.py` writes "Running mypy on PyPy is
not supported yet" and calls sys.exit(2) before importing anything else --
so requiring it on every leg of the matrix bought nothing. It only ever
runs in the typecheck job.

It moves out of requirements-tests.txt into requirements-typecheck.txt,
which requirements.txt does not include, and the typecheck job installs
explicitly. The `platform_python_implementation != "PyPy"` marker goes
away with it: the PyPy legs no longer see mypy at all, so there is also
nothing to fail when pip tries to build mypy 2.x's ast-serialize
extension, which ships no PyPy wheels.

scripts/typecheck.sh now says how to install mypy instead of dying with
"command not found", and CONTRIBUTING.md documents the extra step. It
also claimed ./scripts/format.sh "uses ruff & mypy"; it has only run
ruff since the type checks were commented out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017q5Vvxifyg39rFZ6x2YUA7
tests/functional/test_typecheck.py shells out to `mypy -p mode` and
fails with mypy's output. It skips when mypy is not importable, so the
suite still runs for anyone who has not installed the optional
requirements-typecheck.txt, and skips outright on PyPy -- mypy.main
calls sys.exit(2) at import there, so it must not be imported in
process.

CI installs mypy on the non-PyPy legs, which turns that skip into a real
run. The type checks now happen once per CPython version rather than
against a single interpreter, which matters here: mypy resolves
`sys.version_info` branches against the running interpreter, so 3.10 and
3.14 do not check the same code.

That makes the separate typecheck job redundant, so it goes, and the
branch-protection job needs only `tests` again. scripts/typecheck.sh
stays for running the check directly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017q5Vvxifyg39rFZ6x2YUA7
@wbarnha
wbarnha merged commit 78274e3 into master Aug 5, 2026
14 checks passed
@wbarnha
wbarnha deleted the claude/mode-issue-4-l89d5h branch August 5, 2026 21:56
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.

Update typehints to allow usage of latest mypy version

2 participants