Files
maple/maple-py/CLAUDE.md
T

2.8 KiB

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).