Make mypy run again, fix what it found, and add one check script
a7fb56cpinned mypy to Python 3.11 for the Pi. In a dev venv that also has the analysis extra, numpy's stubs use 3.12 syntax, so mypy aborted on them before checking a single file - and from the next day on four real errors went in unnoticed (ebab03b,0ef5a04,9fd106b). Skip following numpy/librosa instead; only librosa_analyzer.py touches them. Fix the four errors (variables reused with a narrower type). There is no CI, so scripts/check.sh is the gate: pytest, ruff, mypy, vitest, tsc and eslint in one command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -110,8 +110,10 @@ reported at once.
|
|||||||
|
|
||||||
## Notes for agents
|
## Notes for agents
|
||||||
|
|
||||||
- The backend has `pytest`, `ruff` and `mypy --strict` configured in
|
- There is no CI. `scripts/check.sh` runs every check the repo has - backend
|
||||||
`python-backend/pyproject.toml`, and no CI. Run all three before proposing changes.
|
`pytest`, `ruff` and `mypy --strict`, web `vitest`, `tsc` and `eslint` - and is the
|
||||||
|
gate: run it before proposing changes. A checker that cannot run (mypy once aborted
|
||||||
|
on numpy's stubs for two weeks without anyone noticing) is a failure, not a skip.
|
||||||
- Prefer adding a scenario in `python-backend/scenarios/` over a hand-rolled test when
|
- Prefer adding a scenario in `python-backend/scenarios/` over a hand-rolled test when
|
||||||
the behaviour is end-to-end — those files are executed by the test suite.
|
the behaviour is end-to-end — those files are executed by the test suite.
|
||||||
- `python-backend/notebooks/` is university course material on chord recognition, not
|
- `python-backend/notebooks/` is university course material on chord recognition, not
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ ruff check .
|
|||||||
mypy # --strict, configured in pyproject.toml
|
mypy # --strict, configured in pyproject.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`../scripts/check.sh` runs these three plus the web front-end's checks in one go.
|
||||||
|
|
||||||
`tests/test_wire.py` parses `../esp-firmware/src/Messages.h` and fails if the Python
|
`tests/test_wire.py` parses `../esp-firmware/src/Messages.h` and fails if the Python
|
||||||
message ids drift from the firmware's - the contract is hand-duplicated in two
|
message ids drift from the firmware's - the contract is hand-duplicated in two
|
||||||
languages, and it had already drifted once (`BUTTON_EVENT` was missing on the Python
|
languages, and it had already drifted once (`BUTTON_EVENT` was missing on the Python
|
||||||
|
|||||||
@@ -343,12 +343,12 @@ def shrink_cover(
|
|||||||
for a browser to decode.
|
for a browser to decode.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with Image.open(io.BytesIO(data)) as image:
|
with Image.open(io.BytesIO(data)) as source:
|
||||||
if max(image.size) <= max_px:
|
if max(source.size) <= max_px:
|
||||||
return data
|
return data
|
||||||
# JPEG can decode straight to 1/2, 1/4 or 1/8 size; a no-op for other formats.
|
# JPEG can decode straight to 1/2, 1/4 or 1/8 size; a no-op for other formats.
|
||||||
image.draft("RGB", (max_px, max_px))
|
source.draft("RGB", (max_px, max_px))
|
||||||
image = image.convert("RGB")
|
image = source.convert("RGB")
|
||||||
# `thumbnail` keeps the aspect ratio and never scales up.
|
# `thumbnail` keeps the aspect ratio and never scales up.
|
||||||
image.thumbnail((max_px, max_px), Image.Resampling.LANCZOS)
|
image.thumbnail((max_px, max_px), Image.Resampling.LANCZOS)
|
||||||
buffer = io.BytesIO()
|
buffer = io.BytesIO()
|
||||||
|
|||||||
@@ -135,9 +135,9 @@ def _cover_for(
|
|||||||
return cache.store_cover(identifier, art), art
|
return cache.store_cover(identifier, art), art
|
||||||
for path in paths[:3]:
|
for path in paths[:3]:
|
||||||
# Podcast feeds sometimes art only some episodes; a couple of tries is enough.
|
# Podcast feeds sometimes art only some episodes; a couple of tries is enough.
|
||||||
art = _embedded_art(path)
|
embedded = _embedded_art(path)
|
||||||
if art is not None:
|
if embedded is not None:
|
||||||
return cache.store_cover(identifier, art), art
|
return cache.store_cover(identifier, embedded), embedded
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
@@ -155,9 +155,9 @@ def _cover_for_episode(
|
|||||||
if candidate.is_file():
|
if candidate.is_file():
|
||||||
art = candidate.read_bytes()
|
art = candidate.read_bytes()
|
||||||
return cache.store_cover(identifier, art), art
|
return cache.store_cover(identifier, art), art
|
||||||
art = _embedded_art(path)
|
embedded = _embedded_art(path)
|
||||||
if art is not None:
|
if embedded is not None:
|
||||||
return cache.store_cover(identifier, art), art
|
return cache.store_cover(identifier, embedded), embedded
|
||||||
for name in _COVER_NAMES:
|
for name in _COVER_NAMES:
|
||||||
candidate = folder / name
|
candidate = folder / name
|
||||||
if candidate.is_file():
|
if candidate.is_file():
|
||||||
|
|||||||
@@ -77,3 +77,12 @@ warn_unreachable = true
|
|||||||
# checkout must still type-check clean, so it needs the same treatment as librosa.
|
# checkout must still type-check clean, so it needs the same treatment as librosa.
|
||||||
module = ["vlc", "serial_asyncio", "ruamel.*", "librosa.*", "numpy", "feedparser"]
|
module = ["vlc", "serial_asyncio", "ruamel.*", "librosa.*", "numpy", "feedparser"]
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
# Recent numpy ships stubs written in 3.12+ syntax (`type X = ...`). Checking against
|
||||||
|
# the device's 3.11 makes mypy refuse to parse them and abort before checking anything
|
||||||
|
# at all - which is what happened in a dev venv that has the analysis extra. Treat the
|
||||||
|
# analysis stack as untyped instead: it is only touched by librosa_analyzer.py.
|
||||||
|
module = ["numpy", "numpy.*", "librosa", "librosa.*"]
|
||||||
|
follow_imports = "skip"
|
||||||
|
follow_imports_for_stubs = true
|
||||||
|
|||||||
44
scripts/check.sh
Executable file
44
scripts/check.sh
Executable file
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Every check this repo has, in one command. There is no CI, so this is the gate:
|
||||||
|
# run it before committing, and treat a checker that cannot run as a failure.
|
||||||
|
#
|
||||||
|
# scripts/check.sh backend and web
|
||||||
|
# scripts/check.sh backend just one half
|
||||||
|
# scripts/check.sh web
|
||||||
|
#
|
||||||
|
# PYTHON picks the interpreter (default: python-backend/.venv/bin/python, else python3).
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
what="${1:-all}"
|
||||||
|
|
||||||
|
backend() {
|
||||||
|
cd "$repo/python-backend"
|
||||||
|
local python="${PYTHON:-}"
|
||||||
|
if [[ -z "$python" ]]; then
|
||||||
|
if [[ -x .venv/bin/python ]]; then python=.venv/bin/python; else python=python3; fi
|
||||||
|
fi
|
||||||
|
echo "== backend: pytest"
|
||||||
|
"$python" -m pytest -q
|
||||||
|
echo "== backend: ruff"
|
||||||
|
"$python" -m ruff check .
|
||||||
|
echo "== backend: mypy"
|
||||||
|
"$python" -m mypy
|
||||||
|
}
|
||||||
|
|
||||||
|
web() {
|
||||||
|
cd "$repo/web"
|
||||||
|
echo "== web: vitest"
|
||||||
|
npm run --silent test
|
||||||
|
echo "== web: tsc + eslint"
|
||||||
|
npm run --silent check
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$what" in
|
||||||
|
all) backend; web ;;
|
||||||
|
backend) backend ;;
|
||||||
|
web) web ;;
|
||||||
|
*) echo "usage: $0 [all|backend|web]" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
echo "== all checks passed"
|
||||||
Reference in New Issue
Block a user