Make mypy run again, fix what it found, and add one check script

a7fb56c pinned 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:
2026-10-04 00:22:25 +02:00
parent 14b5133543
commit b791beed9c
6 changed files with 69 additions and 12 deletions

View File

@@ -110,8 +110,10 @@ reported at once.
## Notes for agents
- The backend has `pytest`, `ruff` and `mypy --strict` configured in
`python-backend/pyproject.toml`, and no CI. Run all three before proposing changes.
- There is no CI. `scripts/check.sh` runs every check the repo has - backend
`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
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

View File

@@ -106,6 +106,8 @@ ruff check .
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
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

View File

@@ -343,12 +343,12 @@ def shrink_cover(
for a browser to decode.
"""
try:
with Image.open(io.BytesIO(data)) as image:
if max(image.size) <= max_px:
with Image.open(io.BytesIO(data)) as source:
if max(source.size) <= max_px:
return data
# 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))
image = image.convert("RGB")
source.draft("RGB", (max_px, max_px))
image = source.convert("RGB")
# `thumbnail` keeps the aspect ratio and never scales up.
image.thumbnail((max_px, max_px), Image.Resampling.LANCZOS)
buffer = io.BytesIO()

View File

@@ -135,9 +135,9 @@ def _cover_for(
return cache.store_cover(identifier, art), art
for path in paths[:3]:
# Podcast feeds sometimes art only some episodes; a couple of tries is enough.
art = _embedded_art(path)
if art is not None:
return cache.store_cover(identifier, art), art
embedded = _embedded_art(path)
if embedded is not None:
return cache.store_cover(identifier, embedded), embedded
return None, None
@@ -155,9 +155,9 @@ def _cover_for_episode(
if candidate.is_file():
art = candidate.read_bytes()
return cache.store_cover(identifier, art), art
art = _embedded_art(path)
if art is not None:
return cache.store_cover(identifier, art), art
embedded = _embedded_art(path)
if embedded is not None:
return cache.store_cover(identifier, embedded), embedded
for name in _COVER_NAMES:
candidate = folder / name
if candidate.is_file():

View File

@@ -77,3 +77,12 @@ warn_unreachable = true
# checkout must still type-check clean, so it needs the same treatment as librosa.
module = ["vlc", "serial_asyncio", "ruamel.*", "librosa.*", "numpy", "feedparser"]
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
View 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"