mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25:08 +00:00
* ci: mypy ratchet -- keep type-clean modules clean mypy-clean.txt lists the 71 modules under src/ that type-check clean; scripts/check_types.py runs mypy (--follow-imports=silent) on exactly those files and fails on any error or a missing/unsorted/duplicate entry. A new "Type check (mypy ratchet)" CI job runs it with mypy 1.20.2 and pinned stubs; the manual pre-commit mypy hook now runs the same script (a local hook, so mypy sees the installed requirements like CI does). 35 modules were made clean with annotation-only fixes: hints, typing.cast, TYPE_CHECKING imports, implicit-Optional defaults made explicit, and annotations widened (never guards removed) where mypy called a defensive isinstance check unreachable. No runtime behaviour change. mypy.ini: numpy and orjson are treated as Any (follow_imports=skip, also for stubs). numpy 2.3+ stubs use 3.12 `type` statements that mypy won't parse at python_version 3.10, and orjson is optional, so seeing its stubs made the result depend on whether it was installed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: annotate check_types.py's list-form mypy subprocess Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
100 lines
3.6 KiB
Python
100 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Type-check the modules listed in mypy-clean.txt (the mypy ratchet).
|
|
|
|
Most of src/ still has mypy errors, so CI can't require a clean `mypy src`.
|
|
Instead mypy-clean.txt lists the modules that *are* clean, and this script
|
|
fails if any of them regresses. When you make another module clean, add it to
|
|
the list; nothing ever comes off it.
|
|
|
|
Imports are followed silently: a listed module is checked against the types of
|
|
everything it imports, but errors inside those imported modules are not
|
|
reported, so a clean file isn't failed by an unlisted neighbour.
|
|
|
|
Usage:
|
|
python scripts/check_types.py # check the listed modules
|
|
python scripts/check_types.py --list # print the list and exit
|
|
|
|
Extra arguments after ``--`` are passed to mypy.
|
|
Exit status: 0 clean, 1 mypy errors, 2 a bad list (missing file, duplicate,
|
|
unsorted, or empty).
|
|
"""
|
|
|
|
import argparse
|
|
import subprocess # nosec B404 - list-form argv only, no shell # nosemgrep
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
LIST_FILE = REPO_ROOT / "mypy-clean.txt"
|
|
|
|
|
|
def read_list(path: Path = LIST_FILE) -> list:
|
|
"""The listed paths, in file order, with comments and blank lines dropped."""
|
|
entries = []
|
|
for raw in path.read_text(encoding="utf-8").splitlines():
|
|
line = raw.split("#", 1)[0].strip()
|
|
if line:
|
|
entries.append(line)
|
|
return entries
|
|
|
|
|
|
def list_problems(entries: list, root: Path = REPO_ROOT) -> list:
|
|
"""Why the list can't be used as-is; empty when it is fine."""
|
|
problems = []
|
|
if not entries:
|
|
problems.append(f"{LIST_FILE.name} lists no modules")
|
|
seen = set()
|
|
for entry in entries:
|
|
if entry in seen:
|
|
problems.append(f"listed twice: {entry}")
|
|
seen.add(entry)
|
|
if "\\" in entry:
|
|
problems.append(f"use forward slashes: {entry}")
|
|
elif not (root / entry).is_file():
|
|
problems.append(f"listed but not found (renamed or deleted? update the list): {entry}")
|
|
if entries != sorted(entries):
|
|
problems.append(f"{LIST_FILE.name} is not sorted")
|
|
return problems
|
|
|
|
|
|
def main(argv=None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
|
parser.add_argument("--list", action="store_true", help="print the listed modules and exit")
|
|
parser.add_argument("mypy_args", nargs="*", help="extra mypy arguments (after --)")
|
|
args = parser.parse_args(argv)
|
|
|
|
entries = read_list()
|
|
problems = list_problems(entries)
|
|
if problems:
|
|
for problem in problems:
|
|
print(f"check_types: {problem}", file=sys.stderr)
|
|
return 2
|
|
if args.list:
|
|
print("\n".join(entries))
|
|
return 0
|
|
|
|
cmd = [
|
|
sys.executable, "-m", "mypy",
|
|
"--config-file", str(REPO_ROOT / "mypy.ini"),
|
|
"--follow-imports=silent",
|
|
*args.mypy_args,
|
|
*entries,
|
|
]
|
|
print(f"check_types: mypy on {len(entries)} modules from {LIST_FILE.name}", flush=True)
|
|
# This interpreter's mypy, fixed flags, and paths from the checked-in list.
|
|
result = subprocess.run(cmd, cwd=REPO_ROOT) # nosec B603 - list-form argv, no shell # nosemgrep
|
|
if result.returncode > 1: # mypy itself failed (bad config, crash)
|
|
return result.returncode
|
|
if result.returncode != 0:
|
|
print(
|
|
"check_types: a module on the mypy ratchet has type errors. Fix them "
|
|
f"(annotation-only where possible) rather than taking it off {LIST_FILE.name}.",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|