Skip to content

Commit f869c2d

Browse files
authored
Merge pull request #7 from strohganoff/fix/event-handler-generic-type
Fix typing for events and event handlers
2 parents 3572911 + ccf8310 commit f869c2d

File tree

9 files changed

+150
-78
lines changed

9 files changed

+150
-78
lines changed

streamdeck/actions.py

+14-34
Original file line numberDiff line numberDiff line change
@@ -4,42 +4,21 @@
44
from collections import defaultdict
55
from functools import cached_property
66
from logging import getLogger
7-
from typing import TYPE_CHECKING
7+
from typing import TYPE_CHECKING, cast
8+
9+
from streamdeck.types import BaseEventHandlerFunc, available_event_names
810

911

1012
if TYPE_CHECKING:
1113
from collections.abc import Callable, Generator
1214

13-
from streamdeck.types import EventHandlerFunc, EventNameStr
15+
from streamdeck.models.events import EventBase
16+
from streamdeck.types import EventHandlerFunc, EventNameStr, TEvent_contra
1417

1518

1619
logger = getLogger("streamdeck.actions")
1720

1821

19-
20-
available_event_names: set[EventNameStr] = {
21-
"applicationDidLaunch",
22-
"applicationDidTerminate",
23-
"deviceDidConnect",
24-
"deviceDidDisconnect",
25-
"dialDown",
26-
"dialRotate",
27-
"dialUp",
28-
"didReceiveGlobalSettings",
29-
"didReceiveDeepLink",
30-
"didReceiveSettings",
31-
"keyDown",
32-
"keyUp",
33-
"propertyInspectorDidAppear",
34-
"propertyInspectorDidDisappear",
35-
"systemDidWakeUp",
36-
"titleParametersDidChange",
37-
"touchTap",
38-
"willAppear",
39-
"willDisappear",
40-
}
41-
42-
4322
class ActionBase(ABC):
4423
"""Base class for all actions."""
4524

@@ -49,9 +28,9 @@ def __init__(self):
4928
Args:
5029
uuid (str): The unique identifier for the action.
5130
"""
52-
self._events: dict[EventNameStr, set[EventHandlerFunc]] = defaultdict(set)
31+
self._events: dict[EventNameStr, set[BaseEventHandlerFunc]] = defaultdict(set)
5332

54-
def on(self, event_name: EventNameStr, /) -> Callable[[EventHandlerFunc], EventHandlerFunc]:
33+
def on(self, event_name: EventNameStr, /) -> Callable[[EventHandlerFunc[TEvent_contra] | BaseEventHandlerFunc], EventHandlerFunc[TEvent_contra] | BaseEventHandlerFunc]:
5534
"""Register an event handler for a specific event.
5635
5736
Args:
@@ -67,14 +46,15 @@ def on(self, event_name: EventNameStr, /) -> Callable[[EventHandlerFunc], EventH
6746
msg = f"Provided event name for action handler does not exist: {event_name}"
6847
raise KeyError(msg)
6948

70-
def _wrapper(func: EventHandlerFunc) -> EventHandlerFunc:
71-
self._events[event_name].add(func)
49+
def _wrapper(func: EventHandlerFunc[TEvent_contra]) -> EventHandlerFunc[TEvent_contra]:
50+
# Cast to BaseEventHandlerFunc so that the storage type is consistent.
51+
self._events[event_name].add(cast(BaseEventHandlerFunc, func))
7252

7353
return func
7454

7555
return _wrapper
7656

77-
def get_event_handlers(self, event_name: EventNameStr, /) -> Generator[EventHandlerFunc, None, None]:
57+
def get_event_handlers(self, event_name: EventNameStr, /) -> Generator[EventHandlerFunc[EventBase], None, None]:
7858
"""Get all event handlers for a specific event.
7959
8060
Args:
@@ -133,12 +113,12 @@ def register(self, action: ActionBase) -> None:
133113
"""
134114
self._plugin_actions.append(action)
135115

136-
def get_action_handlers(self, event_name: EventNameStr, event_action_uuid: str | None = None) -> Generator[EventHandlerFunc, None, None]:
116+
def get_action_handlers(self, event_name: EventNameStr, event_action_uuid: str | None = None) -> Generator[EventHandlerFunc[EventBase], None, None]:
137117
"""Get all event handlers for a specific event from all registered actions.
138118
139119
Args:
140120
event_name (EventName): The name of the event to retrieve handlers for.
141-
event_action_uuid (str | None): The action UUID to get handlers for.
121+
event_action_uuid (str | None): The action UUID to get handlers for.
142122
If None (i.e., the event is not action-specific), get all handlers for the event.
143123
144124
Yields:
@@ -148,7 +128,7 @@ def get_action_handlers(self, event_name: EventNameStr, event_action_uuid: str |
148128
# If the event is action-specific (i.e is not a GlobalAction and has a UUID attribute),
149129
# only get handlers for that action, as we don't want to trigger
150130
# and pass this event to handlers for other actions.
151-
if event_action_uuid is not None and (hasattr(action, "uuid") and action.uuid != event_action_uuid):
131+
if event_action_uuid is not None and (isinstance(action, Action) and action.uuid != event_action_uuid):
152132
continue
153133

154134
yield from action.get_event_handlers(event_name)

streamdeck/manager.py

+19-11
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
from __future__ import annotations
22

33
import functools
4-
import inspect
5-
import logging
64
from logging import getLogger
75
from typing import TYPE_CHECKING
86

97
from streamdeck.actions import ActionRegistry
108
from streamdeck.command_sender import StreamDeckCommandSender
119
from streamdeck.models.events import ContextualEventMixin, event_adapter
10+
from streamdeck.types import (
11+
EventHandlerBasicFunc,
12+
EventHandlerFunc,
13+
TEvent_contra,
14+
is_bindable_handler,
15+
is_valid_event_name,
16+
)
1217
from streamdeck.utils.logging import configure_streamdeck_logger
1318
from streamdeck.websocket import WebSocketClient
1419

1520

1621
if TYPE_CHECKING:
17-
from collections.abc import Callable
1822
from typing import Any, Literal
1923

2024
from streamdeck.actions import Action
@@ -70,7 +74,7 @@ def register_action(self, action: Action) -> None:
7074

7175
self._registry.register(action)
7276

73-
def _inject_command_sender(self, handler: Callable[..., None], command_sender: StreamDeckCommandSender) -> Callable[..., None]:
77+
def _inject_command_sender(self, handler: EventHandlerFunc[TEvent_contra], command_sender: StreamDeckCommandSender) -> EventHandlerBasicFunc[TEvent_contra]:
7478
"""Inject command_sender into handler if it accepts it as a parameter.
7579
7680
Args:
@@ -80,10 +84,7 @@ def _inject_command_sender(self, handler: Callable[..., None], command_sender: S
8084
Returns:
8185
The handler with command_sender injected if needed
8286
"""
83-
args: dict[str, inspect.Parameter] = inspect.signature(handler).parameters
84-
85-
# Check dynamically if the `command_sender`'s name is in the handler's arguments.
86-
if "command_sender" in args:
87+
if is_bindable_handler(handler):
8788
return functools.partial(handler, command_sender=command_sender)
8889

8990
return handler
@@ -101,13 +102,20 @@ def run(self) -> None:
101102

102103
for message in client.listen_forever():
103104
data: EventBase = event_adapter.validate_json(message)
105+
106+
if not is_valid_event_name(data.event):
107+
logger.error("Received event name is not valid: %s", data.event)
108+
continue
109+
104110
logger.debug("Event received: %s", data.event)
105111

106112
# If the event is action-specific, we'll pass the action's uuid to the handler to ensure only the correct action is triggered.
107113
event_action_uuid = data.action if isinstance(data, ContextualEventMixin) else None
108114

109-
for handler in self._registry.get_action_handlers(event_name=data.event, event_action_uuid=event_action_uuid):
110-
handler = self._inject_command_sender(handler, command_sender)
115+
for event_handler in self._registry.get_action_handlers(event_name=data.event, event_action_uuid=event_action_uuid):
116+
processed_handler = self._inject_command_sender(event_handler, command_sender)
111117
# TODO: from contextual event occurences, save metadata to the action's properties.
112118

113-
handler(data)
119+
processed_handler(data)
120+
121+

streamdeck/models/events.py

+20-20
Original file line numberDiff line numberDiff line change
@@ -29,82 +29,82 @@ class DeviceSpecificEventMixin:
2929

3030

3131
class ApplicationDidLaunch(EventBase):
32-
event: Literal["applicationDidLaunch"]
32+
event: Literal["applicationDidLaunch"] # type: ignore[override]
3333
payload: dict[Literal["application"], str]
3434
"""Payload containing the name of the application that triggered the event."""
3535

3636

3737
class ApplicationDidTerminate(EventBase):
38-
event: Literal["applicationDidTerminate"]
38+
event: Literal["applicationDidTerminate"] # type: ignore[override]
3939
payload: dict[Literal["application"], str]
4040
"""Payload containing the name of the application that triggered the event."""
4141

4242

4343
class DeviceDidConnect(EventBase, DeviceSpecificEventMixin):
44-
event: Literal["deviceDidConnect"]
44+
event: Literal["deviceDidConnect"] # type: ignore[override]
4545
deviceInfo: dict[str, Any]
4646
"""Information about the newly connected device."""
4747

4848

4949
class DeviceDidDisconnect(EventBase, DeviceSpecificEventMixin):
50-
event: Literal["deviceDidDisconnect"]
50+
event: Literal["deviceDidDisconnect"] # type: ignore[override]
5151

5252

5353
class DialDown(EventBase, ContextualEventMixin, DeviceSpecificEventMixin):
54-
event: Literal["dialDown"]
54+
event: Literal["dialDown"] # type: ignore[override]
5555
payload: dict[str, Any]
5656

5757

5858
class DialRotate(EventBase, ContextualEventMixin, DeviceSpecificEventMixin):
59-
event: Literal["dialRotate"]
59+
event: Literal["dialRotate"] # type: ignore[override]
6060
payload: dict[str, Any]
6161

6262

6363
class DialUp(EventBase, ContextualEventMixin, DeviceSpecificEventMixin):
64-
event: Literal["dialUp"]
64+
event: Literal["dialUp"] # type: ignore[override]
6565
payload: dict[str, Any]
6666

6767

6868
class DidReceiveDeepLink(EventBase):
69-
event: Literal["didReceiveDeepLink"]
69+
event: Literal["didReceiveDeepLink"] # type: ignore[override]
7070
payload: dict[Literal["url"], str]
7171

7272

7373
class DidReceiveGlobalSettings(EventBase):
74-
event: Literal["didReceiveGlobalSettings"]
74+
event: Literal["didReceiveGlobalSettings"] # type: ignore[override]
7575
payload: dict[Literal["settings"], dict[str, Any]]
7676

7777

7878
class DidReceivePropertyInspectorMessage(EventBase, ContextualEventMixin):
79-
event: Literal["sendToPlugin"]
79+
event: Literal["sendToPlugin"] # type: ignore[override]
8080
payload: dict[str, Any]
8181

8282

8383
class DidReceiveSettings(EventBase, ContextualEventMixin, DeviceSpecificEventMixin):
84-
event: Literal["didReceiveSettings"]
84+
event: Literal["didReceiveSettings"] # type: ignore[override]
8585
payload: dict[str, Any]
8686

8787

8888
class KeyDown(EventBase, ContextualEventMixin, DeviceSpecificEventMixin):
89-
event: Literal["keyDown"]
89+
event: Literal["keyDown"] # type: ignore[override]
9090
payload: dict[str, Any]
9191

9292

9393
class KeyUp(EventBase, ContextualEventMixin, DeviceSpecificEventMixin):
94-
event: Literal["keyUp"]
94+
event: Literal["keyUp"] # type: ignore[override]
9595
payload: dict[str, Any]
9696

9797

9898
class PropertyInspectorDidAppear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin):
99-
event: Literal["propertyInspectorDidAppear"]
99+
event: Literal["propertyInspectorDidAppear"] # type: ignore[override]
100100

101101

102102
class PropertyInspectorDidDisappear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin):
103-
event: Literal["propertyInspectorDidDisappear"]
103+
event: Literal["propertyInspectorDidDisappear"] # type: ignore[override]
104104

105105

106106
class SystemDidWakeUp(EventBase):
107-
event: Literal["systemDidWakeUp"]
107+
event: Literal["systemDidWakeUp"] # type: ignore[override]
108108

109109

110110
class TitleParametersDict(TypedDict):
@@ -127,24 +127,24 @@ class TitleParametersDidChangePayload(TypedDict):
127127

128128

129129
class TitleParametersDidChange(EventBase, DeviceSpecificEventMixin):
130-
event: Literal["titleParametersDidChange"]
130+
event: Literal["titleParametersDidChange"] # type: ignore[override]
131131
context: str
132132
"""Identifies the instance of an action that caused the event, i.e. the specific key or dial."""
133133
payload: TitleParametersDidChangePayload
134134

135135

136136
class TouchTap(EventBase, ContextualEventMixin, DeviceSpecificEventMixin):
137-
event: Literal["touchTap"]
137+
event: Literal["touchTap"] # type: ignore[override]
138138
payload: dict[str, Any]
139139

140140

141141
class WillAppear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin):
142-
event: Literal["willAppear"]
142+
event: Literal["willAppear"] # type: ignore[override]
143143
payload: dict[str, Any]
144144

145145

146146
class WillDisappear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin):
147-
event: Literal["willDisappear"]
147+
event: Literal["willDisappear"] # type: ignore[override]
148148
payload: dict[str, Any]
149149

150150

0 commit comments

Comments
 (0)