Files
maple/maple-py/CLAUDE.md
T
2026-06-22 22:45:04 +10:00

58 lines
2.9 KiB
Markdown

# maple-py conventions
Python conventions for the `maple-py/` package. These OVERRIDE default behavior;
follow them exactly. (General repo guidance is in the root `CLAUDE.md`; 4-space
indentation applies here too.)
The package is pyright-checked (basic mode, Python 3.14). `Track`
(`src/maple/track.py`) and the `Analysis` family (`src/maple/analyze.py`) are the
reference examples for all of the below.
## Imports at the top of the file
All imports go at module top. No lazy/local imports inside functions. Never wrap
an import in `try/except ImportError` to degrade gracefully — if an integral
dependency (e.g. `mixxx_analyzer`, `scipy`) is missing, the process must **die at
startup** via the top-level import failing. The only acceptable indented import
is an `if TYPE_CHECKING:` block to break a circular type-only reference.
## Fail loud — never swallow into None
"Don't swallow into None" is a general principle at every level, including call
sites. An operation that fails must **let the exception propagate** — never catch
it and return `None` / an empty / degraded result. BPM/beat analysis and waveform
extraction are integral: `_mixxx_analyze` raises rather than returning None,
`_decode_pcm` raises (`RuntimeError`) if ffmpeg is missing or fails to decode
rather than returning an empty array, and every `Analysis.analyze` call site (in
`anlz.py`, `cli.py`, `collection/core.py`) lets failures abort the run (including
not catching around `ThreadPoolExecutor` `fut.result()`).
Reserve `None` returns for *legitimate absence* (e.g. a cache miss in
`Analysis.load`), never for *failure*. The one tolerated catch is a genuinely
*optional* side-effect failing (e.g. writing the optimization cache via
`analysis.save()`), which logs and continues because it doesn't affect
correctness.
## Specific types over generic ones
- **Dataclasses, not bare dicts**, for structured values passed around.
- **`pathlib.Path`, not `str`**, for local filesystem paths (strings are fine for
serialization / rekordbox-USB paths that are inherently string data).
- **`typing.NamedTuple` (or a small dataclass), not bare tuples**, for fixed
records (e.g. `Beat(number, tempo_x100, time_ms)`, `Bands(low, mid, high)`).
- **No `None`-valued fields.** Absent values resolve at instantiation to a
concrete sentinel — `0.0` (0.0 = unknown), `""`, `[]`, `b""` — per `Track`.
`None` is acceptable only as a *factory return* signalling absence, never as a
stored field.
Idiomatic dataclass shape: plain `@dataclass` (no frozen/slots), `@property` for
computed views, `@classmethod from_*()` factories, concrete field defaults.
## Type every function parameter
Always annotate every parameter, including nested/inner functions and
`@classmethod` factory args (`self`/`cls` exempt). No bare `def f(res):`. Use the
real type even from a third-party lib — e.g. `mixxx_analyzer.AnalysisResult`
(referenced directly; no extra import needed under
`from __future__ import annotations`).