From edeaf4db46d594d996902ce66986d4effb8d0836 Mon Sep 17 00:00:00 2001 From: Job Date: Fri, 17 Jul 2026 20:44:29 +0200 Subject: [PATCH] docs: add docstrings to all new/modified functions to meet 80% coverage - lib/instruments.py: module docstring, class docstring, method docstrings for all public methods and private validators - lib/tunings.py: docstrings for _build_*, _valid_instrument_ids, _default_profile_id_for_instrument helper functions - lib/routers/instruments.py and lib/routers/stats.py: endpoint docstrings --- lib/instruments.py | 18 ++++++++++++++++++ lib/routers/instruments.py | 1 + lib/routers/stats.py | 3 +-- lib/tunings.py | 6 ++++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/instruments.py b/lib/instruments.py index 95896af..6e6b0d2 100644 --- a/lib/instruments.py +++ b/lib/instruments.py @@ -20,6 +20,7 @@ _PROFILE_PATHWAYS = ("songs", "practice", "learn", "studio") def _validate_str(value, field_name, max_len=128): + """Validate a required non-empty string field, returning stripped value.""" if not isinstance(value, str) or not value.strip(): raise ValueError(f"{field_name} must be a non-empty string") if len(value) > max_len: @@ -28,6 +29,7 @@ def _validate_str(value, field_name, max_len=128): def _validate_optional_str(value, field_name, max_len=256): + """Validate an optional string field; None passes through, non-empty string required otherwise.""" if value is None: return None if not isinstance(value, str): @@ -38,6 +40,7 @@ def _validate_optional_str(value, field_name, max_len=256): def _validate_float_range(value, field_name, lo, hi): + """Validate a numeric value falls within [lo, hi], rejecting bools.""" if not isinstance(value, (int, float)) or isinstance(value, bool): raise ValueError(f"{field_name} must be a number") v = float(value) @@ -47,6 +50,7 @@ def _validate_float_range(value, field_name, lo, hi): def _validate_midi_list(lst, field_name, expected_len): + """Validate a list of MIDI note numbers (ints in 0-127) of the given length.""" if not isinstance(lst, list): raise ValueError(f"{field_name} must be a list") if len(lst) != expected_len: @@ -63,6 +67,7 @@ def _validate_midi_list(lst, field_name, expected_len): def _validate_offset_list(lst, field_name, expected_len): + """Validate a list of semitone offsets (ints in -12..12) of the given length.""" if not isinstance(lst, list): raise ValueError(f"{field_name} must be a list of offsets") if len(lst) != expected_len: @@ -242,17 +247,25 @@ class InstrumentRegistry: log.info("registered instrument %r (%s)", inst_id, label) def unregister(self, instrument_id: str): + """Remove an instrument definition from the registry.""" if instrument_id in self._instruments: del self._instruments[instrument_id] log.info("unregistered instrument %r", instrument_id) def get(self, instrument_id: str) -> dict | None: + """Return the definition for a single instrument, or None.""" return self._instruments.get(instrument_id) def get_all(self) -> list[dict]: + """Return all registered instrument definitions.""" return list(self._instruments.values()) def compute_tuning_midis(self, instrument_id: str, string_count: int, tuning_name: str) -> list[int] | None: + """Return absolute MIDI notes for a named tuning on a stringed instrument. + + Computed by adding the tuning's semitone offsets to the instrument's + standard open-string MIDI notes for the given string count. + """ inst = self._instruments.get(instrument_id) if not inst or inst["kind"] != "stringed": return None @@ -264,18 +277,21 @@ class InstrumentRegistry: return [s + o for s, o in zip(std, offsets)] def get_tuning_names(self, instrument_id: str, string_count: int) -> list[str]: + """Return all tuning preset names for a stringed instrument + string count.""" inst = self._instruments.get(instrument_id) if not inst or inst["kind"] != "stringed": return [] return list((inst["tunings"].get(str(string_count)) or {}).keys()) def get_standard_midis(self, instrument_id: str, string_count: int) -> list[int] | None: + """Return standard open-string MIDI notes for a stringed instrument + string count.""" inst = self._instruments.get(instrument_id) if not inst or inst["kind"] != "stringed": return None return inst["standard_tunings"].get(str(string_count)) def get_default_role(self, instrument_id: str) -> str | None: + """Return the id of the default role for an instrument, or None.""" inst = self._instruments.get(instrument_id) if not inst: return None @@ -287,6 +303,7 @@ class InstrumentRegistry: return None def find_role_by_arrangement(self, instrument_id: str, arr_name: str, arr_flags: dict = None) -> str | None: + """Find the role id matching an arrangement name or path flags for a specific instrument.""" inst = self._instruments.get(instrument_id) if not inst: return None @@ -301,6 +318,7 @@ class InstrumentRegistry: return None def instrument_id_for_arrangement(self, arr_name: str, arr_flags: dict = None) -> str | None: + """Return the instrument id whose roles match a given arrangement name or flags.""" name_lower = (arr_name or "").strip().lower() for inst in self._instruments.values(): for role in inst["roles"]: diff --git a/lib/routers/instruments.py b/lib/routers/instruments.py index e4e5946..2f7074e 100644 --- a/lib/routers/instruments.py +++ b/lib/routers/instruments.py @@ -12,5 +12,6 @@ router = APIRouter() @router.get("/api/instruments") def get_instruments(): + """Return all instrument definitions registered via instrument plugins.""" reg = getattr(appstate, "instrument_registry", None) return reg.get_all() if reg else [] diff --git a/lib/routers/stats.py b/lib/routers/stats.py index 2882f71..557ca72 100644 --- a/lib/routers/stats.py +++ b/lib/routers/stats.py @@ -243,8 +243,7 @@ def api_stats_best(): @router.get("/api/stats/best-by-arrangement") def api_stats_best_by_arrangement(): - """{filename: {arrangement_index: best_accuracy}} per-arrangement accuracy, - for per-role badging and hover overlays on the library grid.""" + """Return {filename: {arrangement_index: best_accuracy}} for per-role badging.""" return appstate.meta_db.arrangement_accuracy_map() diff --git a/lib/tunings.py b/lib/tunings.py index fd489aa..e6806fb 100644 --- a/lib/tunings.py +++ b/lib/tunings.py @@ -28,6 +28,7 @@ def set_instrument_registry(reg): def _build_standard_midis(registry=None): + """Build {instrument_key: [midis]} from the registry, falling back to hardcoded STANDARD_OPEN_MIDIS.""" reg = registry or _instrument_registry result = {} if reg: @@ -43,6 +44,7 @@ def _build_standard_midis(registry=None): def _build_preset_midis(registry=None): + """Build {instrument_key: {tuning_name: [midis]}} from the registry, falling back to TUNING_PRESET_MIDIS.""" reg = registry or _instrument_registry result = {} if reg: @@ -66,6 +68,7 @@ def _build_preset_midis(registry=None): def _build_profile_defaults(registry=None): + """Build {profile_id: profile_dict} from registered instruments, falling back to PROFILE_DEFAULTS.""" reg = registry or _instrument_registry result = {} if reg: @@ -97,11 +100,13 @@ def _build_profile_defaults(registry=None): def _build_profile_ids(registry=None): + """Return a tuple of valid instrument profile ids, derived from the registry.""" profiles = _build_profile_defaults(registry) return tuple(profiles.keys()) def _valid_instrument_ids(registry=None): + """Return the set of valid instrument IDs from the registry, or a guitar/bass fallback.""" reg = registry or _instrument_registry if reg: ids = {inst["id"] for inst in reg.get_all()} @@ -111,6 +116,7 @@ def _valid_instrument_ids(registry=None): def _default_profile_id_for_instrument(instrument_id, registry=None): + """Return the profile id for an instrument's default role, e.g. 'guitar-lead'.""" profiles = _build_profile_defaults(registry) for pid, profile in profiles.items(): if profile["instrument"] == instrument_id and profile.get("default_role"):