Files
musicmouse/python-backend/musicmouse/reactions/registry.py
Martin Bauer a7fb56c9fe Target the Python that Raspberry Pi OS ships
Requiring 3.13 meant the device needed an interpreter the distribution does not
have, which is what dragged uv in, and uv then had to be matched to the Pi's
32-bit userland by hand and to build Pillow from source because no armv7 wheel
exists for a 3.13 ABI. Dropping to 3.11 removes all of that: apt provides the
interpreter and piwheels has prebuilt armhf wheels for the native dependencies.

The 3.13-only syntax was shallow - PEP 695 throughout, which converts back
mechanically:

  type X = Y               ->  X: TypeAlias = Y
  type Handler[E: Event]   ->  E = TypeVar("E", bound=Event) plus a plain alias,
                               which is generic anyway because it carries a TypeVar
  def f[T: Bound](...)     ->  a module-level TypeVar

Also drop the one @override (3.12, and static-only), and stop the lirc test
harness calling Server.close_clients(), which is 3.13: the scripted handler now
releases its connection when asked, which is what that call was there to force.

Verified on 3.11.14 - 485 passed, mypy strict clean - and still 496 passed on
the 3.14 dev venv, which additionally has the analysis extra.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 21:20:59 +02:00

58 lines
1.7 KiB
Python

"""The ``@on`` decorator and the binding step.
Kept in its own module so the reaction modules can import ``on`` without importing the
package that imports them.
"""
from __future__ import annotations
import logging
from collections.abc import Callable, Coroutine
from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar
from musicmouse.bus import EventBus
from musicmouse.events import Event
if TYPE_CHECKING:
from musicmouse.app import App
_log = logging.getLogger(__name__)
__all__ = ["Reaction", "on", "register_all", "registered"]
E = TypeVar("E", bound=Event)
Reaction: TypeAlias = Callable[[E, "App"], Coroutine[Any, Any, None] | None]
_REGISTRY: list[tuple[type[Event], Reaction[Any]]] = []
def on(event_type: type[E]) -> Callable[[Reaction[E]], Reaction[E]]:
"""Register a reaction for ``event_type`` (and any subclass of it)."""
def decorator(reaction: Reaction[E]) -> Reaction[E]:
_REGISTRY.append((event_type, reaction))
return reaction
return decorator
def registered() -> list[tuple[type[Event], Reaction[Any]]]:
return list(_REGISTRY)
def register_all(bus: EventBus, app: App) -> None:
"""Subscribe every declared reaction, with ``app`` bound as its second argument."""
for event_type, reaction in _REGISTRY:
bus.subscribe(event_type, _bind(reaction, app))
_log.debug("Registered %d reactions", len(_REGISTRY))
def _bind(reaction: Reaction[E], app: App) -> Callable[[E], Any]:
def handler(event: E) -> Any:
return reaction(event, app)
# Keep the reaction's name, so a failing handler is identifiable in the log.
handler.__qualname__ = getattr(reaction, "__qualname__", repr(reaction))
return handler