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
This commit is contained in:
Job 2026-07-17 20:44:29 +02:00
parent 3fb69d8595
commit edeaf4db46
4 changed files with 26 additions and 2 deletions

View File

@ -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"]:

View File

@ -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 []

View File

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

View File

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