Make mypy pass and enforce type checks in CI - #83
Merged
Conversation
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
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.
Closes #4.
mypy -p modereported 127 errors under a current mypy; thetype 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:
want_seconds,rate,level_number,level_name) annotated their first parameter with only the fallback'sown type, so registering
str/timedeltawas rejected and everycaller passing
Seconds/Severityfailed. They now name the fullunion they dispatch over.
mode/locals.pywere written against older typeshedsignatures:
Coroutine.throw/AsyncGenerator.athroware overloaded,Mapping.items()/keys()return views, andMutableSet/MutableSequencein-place operators returnSelf. The twothrowerrors quoted in the issue are among these.
mode/utils/tracebacks.pyreadgi_frame/cr_frame/ag_frame/ag_await/gi_yieldfrom/cr_awaitoff the abstract protocols, whichdo not declare them; it now casts to the concrete
types.*Type. Thoseattributes are
Optional, so the frame getters say so.Heapis bound toSupportsRichComparison:heapqorders elements bycomparing them.
FlowControlQueue/ThrowableQueueused an unbound type variable intheir signatures; they are now generic, so
ThrowableQueue[int]meanswhat it says.
ServiceT.beaconis abstract like every other property on the type.ServiceBase._format_logalready requires it;ServiceandServiceProxyboth implement it.FileLogProxymatchesTextIO:line_bufferingis a property,read/readline/readlines/writearestr, notAnyStr.asyncio.Task.all_tasks/current_taskfallbacks and 13 stale
# type: ignorecomments.Service._actually_start/itertimerreadshould_stopinto a localbefore 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