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
|
||||
|
||||
- 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
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