56 lines
2.8 KiB
Markdown
56 lines
2.8 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 is integral:
|
|
`_mixxx_analyze` raises rather than returning None, 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`).
|