Skip to content

Commit f483ed4

Browse files
authored
Merge pull request #17 from strohganoff/refactor/event-handler-catalogs
Refactor/event handler catalogs
2 parents 5cf5d07 + e75846b commit f483ed4

16 files changed

Lines changed: 288 additions & 154 deletions

streamdeck/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from . import (
2-
actions,
32
command_sender,
43
manager,
54
models,
65
utils,
76
websocket,
87
)
8+
from .event_handlers import actions
99

1010

1111
__all__ = [
Lines changed: 13 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,32 @@
66
from logging import getLogger
77
from typing import TYPE_CHECKING, cast
88

9+
from streamdeck.event_handlers.protocol import (
10+
EventHandlerFunc,
11+
EventModel_contra,
12+
InjectableParams,
13+
SupportsEventHandlers,
14+
)
15+
916

1017
if TYPE_CHECKING:
1118
from collections.abc import Callable, Generator
12-
from typing import Protocol
13-
14-
from typing_extensions import ParamSpec, TypeVar
1519

16-
from streamdeck.models.events import EventBase
1720
from streamdeck.types import ActionUUIDStr, EventNameStr
1821

1922

2023

21-
EventModel_contra = TypeVar("EventModel_contra", bound=EventBase, default=EventBase, contravariant=True)
22-
InjectableParams = ParamSpec("InjectableParams", default=...)
23-
24-
class EventHandlerFunc(Protocol[EventModel_contra, InjectableParams]):
25-
"""Protocol for an event handler function that takes an event (of subtype of EventBase) and other parameters that are injectable."""
26-
def __call__(self, event_data: EventModel_contra, *args: InjectableParams.args, **kwargs: InjectableParams.kwargs) -> None: ...
27-
28-
29-
3024
logger = getLogger("streamdeck.actions")
3125

3226

33-
class ActionBase(ABC):
27+
class ActionBase(ABC, SupportsEventHandlers):
3428
"""Base class for all actions."""
29+
_events: dict[EventNameStr, set[EventHandlerFunc]]
30+
"""Dictionary mapping event names to sets of event handler functions."""
3531

3632
def __init__(self) -> None:
3733
"""Initialize an Action instance."""
38-
self._events: dict[EventNameStr, set[EventHandlerFunc]] = defaultdict(set)
34+
self._events = defaultdict(set)
3935

4036
def on(self, event_name: EventNameStr, /) -> Callable[[EventHandlerFunc[EventModel_contra, InjectableParams]], EventHandlerFunc[EventModel_contra, InjectableParams]]:
4137
"""Register an event handler for a specific event.
@@ -88,6 +84,8 @@ class GlobalAction(ActionBase):
8884

8985
class Action(ActionBase):
9086
"""Represents an action that can be performed for a specific action, with event handlers for specific event types."""
87+
uuid: ActionUUIDStr
88+
"""The unique identifier for the action."""
9189

9290
def __init__(self, uuid: ActionUUIDStr) -> None:
9391
"""Initialize an Action instance.
@@ -104,37 +102,3 @@ def name(self) -> str:
104102
return self.uuid.split(".")[-1]
105103

106104

107-
class ActionRegistry:
108-
"""Manages the registration and retrieval of actions and their event handlers."""
109-
110-
def __init__(self) -> None:
111-
"""Initialize an ActionRegistry instance."""
112-
self._plugin_actions: list[ActionBase] = []
113-
114-
def register(self, action: ActionBase) -> None:
115-
"""Register an action with the registry.
116-
117-
Args:
118-
action (Action): The action to register.
119-
"""
120-
self._plugin_actions.append(action)
121-
122-
def get_action_handlers(self, event_name: EventNameStr, event_action_uuid: ActionUUIDStr | None = None) -> Generator[EventHandlerFunc, None, None]:
123-
"""Get all event handlers for a specific event from all registered actions.
124-
125-
Args:
126-
event_name (EventName): The name of the event to retrieve handlers for.
127-
event_action_uuid (str | None): The action UUID to get handlers for.
128-
If None (i.e., the event is not action-specific), get all handlers for the event.
129-
130-
Yields:
131-
EventHandlerFunc: The event handler functions for the specified event.
132-
"""
133-
for action in self._plugin_actions:
134-
# If the event is action-specific (i.e is not a GlobalAction and has a UUID attribute),
135-
# only get handlers for that action, as we don't want to trigger
136-
# and pass this event to handlers for other actions.
137-
if event_action_uuid is not None and (isinstance(action, Action) and action.uuid != event_action_uuid):
138-
continue
139-
140-
yield from action.get_event_handlers(event_name)
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from __future__ import annotations
2+
3+
import inspect
4+
from collections.abc import Iterable
5+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
6+
7+
from typing_extensions import ParamSpec, TypeGuard, TypeVar # noqa: UP035
8+
9+
from streamdeck.command_sender import StreamDeckCommandSender
10+
from streamdeck.models.events import EventBase
11+
12+
13+
if TYPE_CHECKING:
14+
from collections.abc import Iterable
15+
16+
from streamdeck.types import EventNameStr
17+
18+
19+
20+
EventModel_contra = TypeVar("EventModel_contra", bound=EventBase, default=EventBase, contravariant=True)
21+
InjectableParams = ParamSpec("InjectableParams", default=...)
22+
23+
24+
class EventHandlerFunc(Protocol[EventModel_contra, InjectableParams]):
25+
"""Protocol for an event handler function that takes an event (of subtype of EventBase) and other parameters that are injectable."""
26+
def __call__(self, event_data: EventModel_contra, *args: InjectableParams.args, **kwargs: InjectableParams.kwargs) -> None: ...
27+
28+
29+
BindableEventHandlerFunc = EventHandlerFunc[EventModel_contra, tuple[StreamDeckCommandSender]] # type: ignore[misc]
30+
"""Type alias for a bindable event handler function that takes an event (of subtype of EventBase) and a command_sender parameter that is to be injected."""
31+
BoundEventHandlerFunc = EventHandlerFunc[EventModel_contra]
32+
"""Type alias for a bound event handler function that takes an event (of subtype of EventBase) and no other parameters.
33+
34+
Typically used for event handlers that have already had parameters injected.
35+
"""
36+
37+
38+
@runtime_checkable
39+
class SupportsEventHandlers(Protocol):
40+
"""Protocol for a class that holds event-specific handler functions which can be pulled out by event name.
41+
42+
Implementing classes should handle defining the registration of event handlers and ensuring
43+
that they can be retrieved efficiently.
44+
"""
45+
46+
def get_event_handlers(self, event_name: EventNameStr, /) -> Iterable[EventHandlerFunc]:
47+
"""Get all event handlers for a specific event.
48+
49+
Args:
50+
event_name (str): The name of the event to get handlers for.
51+
52+
Returns:
53+
Iterable[EventHandlerFunc]: The event handler functions for the specified event.
54+
"""
55+
...
56+
57+
def get_registered_event_names(self) -> list[EventNameStr]:
58+
"""Get all event names for which handlers are registered.
59+
60+
Returns:
61+
list[str]: A list of event names for which handlers are registered.
62+
"""
63+
...
64+
65+
66+
# def is_bindable_handler(handler: EventHandlerFunc[EventModel_contra, InjectableParams]) -> TypeGuard[BindableEventHandlerFunc[EventModel_contra]]:
67+
def is_bindable_handler(handler: EventHandlerFunc[EventModel_contra, InjectableParams]) -> bool:
68+
"""Check if the handler is prebound with the `command_sender` parameter."""
69+
# Check dynamically if the `command_sender`'s name is in the handler's arguments.
70+
return "command_sender" in inspect.signature(handler).parameters
71+
72+
73+
def is_not_bindable_handler(handler: EventHandlerFunc[EventModel_contra, InjectableParams]) -> TypeGuard[BoundEventHandlerFunc[EventModel_contra]]:
74+
"""Check if the handler only accepts the event_data parameter.
75+
76+
If this function returns False after the is_bindable_handler check is True, then the function has invalid parameters, and will subsequently need to be handled in the calling code.
77+
"""
78+
handler_params = inspect.signature(handler).parameters
79+
return len(handler_params) == 1 and "event_data" in handler_params
80+
81+
82+
83+
84+
85+
86+
87+
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
from streamdeck.event_handlers.actions import Action
6+
7+
8+
if TYPE_CHECKING:
9+
from collections.abc import Generator
10+
11+
from streamdeck.event_handlers.protocol import EventHandlerFunc, SupportsEventHandlers
12+
from streamdeck.types import ActionUUIDStr, EventNameStr
13+
14+
15+
class HandlersRegistry:
16+
"""Manages the registration and retrieval of event handler catalogs and their event handlers."""
17+
_plugin_event_handler_catalogs: list[SupportsEventHandlers]
18+
"""List of registered actions and other event handler catalogs."""
19+
20+
def __init__(self) -> None:
21+
"""Initialize a HandlersRegistry instance."""
22+
self._plugin_event_handler_catalogs = []
23+
24+
def register(self, catalog: SupportsEventHandlers) -> None:
25+
"""Register an event handler catalog with the registry.
26+
27+
Args:
28+
catalog (SupportsEventHandlers): The event handler catalog to register.
29+
"""
30+
self._plugin_event_handler_catalogs.append(catalog)
31+
32+
def get_event_handlers(self, event_name: EventNameStr, event_action_uuid: ActionUUIDStr | None = None) -> Generator[EventHandlerFunc, None, None]:
33+
"""Get all event handlers for a specific event from all registered event handler catalogs.
34+
35+
Args:
36+
event_name (EventName): The name of the event to retrieve handlers for.
37+
event_action_uuid (str | None): The action UUID to get handlers for.
38+
If None (i.e., the event is not action-specific), get all handlers for the event.
39+
40+
Yields:
41+
EventHandlerFunc: The event handler functions for the specified event.
42+
"""
43+
for catalog in self._plugin_event_handler_catalogs:
44+
# If the event is action-specific (i.e is not a GlobalAction and has a UUID attribute),
45+
# only get handlers for that action, as we don't want to trigger
46+
# and pass this event to handlers for other actions.
47+
if event_action_uuid is not None and (isinstance(catalog, Action) and catalog.uuid != event_action_uuid):
48+
continue
49+
50+
yield from catalog.get_event_handlers(event_name)

0 commit comments

Comments
 (0)