Skip to content

Commit 35c0bdc

Browse files
committed
Update EventHandlerFunc protocols. Delete streamdeck/types.py
1 parent 8efe19d commit 35c0bdc

7 files changed

Lines changed: 69 additions & 95 deletions

File tree

streamdeck/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
command_sender,
44
manager,
55
models,
6-
types,
76
utils,
87
websocket,
98
)
@@ -14,7 +13,6 @@
1413
"command_sender",
1514
"manager",
1615
"models",
17-
"types",
1816
"utils",
1917
"websocket",
2018
]

streamdeck/actions.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,27 @@
99

1010
if TYPE_CHECKING:
1111
from collections.abc import Callable, Generator
12+
from typing import Protocol
13+
14+
from typing_extensions import ParamSpec, TypeAlias, TypeVar # noqa: UP035
1215

1316
from streamdeck.models.events import EventBase
14-
from streamdeck.models.events.base import LiteralStrGenericAlias
15-
from streamdeck.types import BaseEventHandlerFunc, EventHandlerFunc, EventNameStr, TEvent_contra
17+
18+
19+
EventNameStr: TypeAlias = str # noqa: UP040
20+
"""Type alias for the event name string.
21+
22+
We don't define literal string values here, as the list of available event names can be added to dynamically.
23+
"""
24+
25+
26+
EventModel_contra = TypeVar("EventModel_contra", bound=EventBase, default=EventBase, contravariant=True)
27+
InjectableParams = ParamSpec("InjectableParams", default=...)
28+
29+
class EventHandlerFunc(Protocol[EventModel_contra, InjectableParams]):
30+
"""Protocol for an event handler function that takes an event (of subtype of EventBase) and other parameters that are injectable."""
31+
def __call__(self, event_data: EventModel_contra, *args: InjectableParams.args, **kwargs: InjectableParams.kwargs) -> None: ...
32+
1633

1734

1835
logger = getLogger("streamdeck.actions")
@@ -27,9 +44,9 @@ def __init__(self) -> None:
2744
Args:
2845
uuid (str): The unique identifier for the action.
2946
"""
30-
self._events: dict[EventNameStr, set[BaseEventHandlerFunc]] = defaultdict(set)
47+
self._events: dict[EventNameStr, set[EventHandlerFunc]] = defaultdict(set)
3148

32-
def on(self, event_name: EventNameStr, /) -> Callable[[EventHandlerFunc[TEvent_contra] | BaseEventHandlerFunc], EventHandlerFunc[TEvent_contra] | BaseEventHandlerFunc]:
49+
def on(self, event_name: EventNameStr, /) -> Callable[[EventHandlerFunc[EventModel_contra, InjectableParams]], EventHandlerFunc[EventModel_contra, InjectableParams]]:
3350
"""Register an event handler for a specific event.
3451
3552
Args:
@@ -41,15 +58,14 @@ def on(self, event_name: EventNameStr, /) -> Callable[[EventHandlerFunc[TEvent_c
4158
Raises:
4259
KeyError: If the provided event name is not available.
4360
"""
44-
def _wrapper(func: EventHandlerFunc[TEvent_contra]) -> EventHandlerFunc[TEvent_contra]:
45-
# Cast to BaseEventHandlerFunc so that the storage type is consistent.
46-
self._events[event_name].add(cast("BaseEventHandlerFunc", func))
47-
61+
def _wrapper(func: EventHandlerFunc[EventModel_contra, InjectableParams]) -> EventHandlerFunc[EventModel_contra, InjectableParams]:
62+
# Cast to EventHandlerFunc with default type arguments so that the storage type is consistent.
63+
self._events[event_name].add(cast("EventHandlerFunc", func))
4864
return func
4965

5066
return _wrapper
5167

52-
def get_event_handlers(self, event_name: EventNameStr, /) -> Generator[EventHandlerFunc[EventBase], None, None]:
68+
def get_event_handlers(self, event_name: EventNameStr, /) -> Generator[EventHandlerFunc, None, None]:
5369
"""Get all event handlers for a specific event.
5470
5571
Args:
@@ -112,7 +128,7 @@ def register(self, action: ActionBase) -> None:
112128
"""
113129
self._plugin_actions.append(action)
114130

115-
def get_action_handlers(self, event_name: EventNameStr, event_action_uuid: str | None = None) -> Generator[EventHandlerFunc[EventBase], None, None]:
131+
def get_action_handlers(self, event_name: EventNameStr, event_action_uuid: str | None = None) -> Generator[EventHandlerFunc, None, None]:
116132
"""Get all event handlers for a specific event from all registered actions.
117133
118134
Args:

streamdeck/manager.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
11
from __future__ import annotations
22

33
import functools
4+
import inspect
45
from logging import getLogger
56
from typing import TYPE_CHECKING
67

78
from pydantic import ValidationError
9+
from typing_extensions import TypeGuard # noqa: UP035
810

911
from streamdeck.actions import Action, ActionBase, ActionRegistry
1012
from streamdeck.command_sender import StreamDeckCommandSender
1113
from streamdeck.event_listener import EventListener, EventListenerManager
1214
from streamdeck.models.events.adapter import EventAdapter
1315
from streamdeck.models.events.common import ContextualEventMixin
14-
from streamdeck.types import (
15-
EventHandlerBasicFunc,
16-
EventHandlerFunc,
17-
TEvent_contra,
18-
is_bindable_handler,
19-
)
2016
from streamdeck.utils.logging import configure_streamdeck_logger
2117
from streamdeck.websocket import WebSocketClient
2218

@@ -25,13 +21,42 @@
2521
from collections.abc import Generator
2622
from typing import Any, Literal
2723

24+
from streamdeck.actions import (
25+
EventHandlerFunc,
26+
EventModel_contra,
27+
InjectableParams,
28+
)
2829
from streamdeck.models.events import EventBase
2930

3031

32+
BindableEventHandlerFunc = EventHandlerFunc[EventModel_contra, [StreamDeckCommandSender]]
33+
"""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."""
34+
BoundEventHandlerFunc = EventHandlerFunc[EventModel_contra, []]
35+
"""Type alias for a bound event handler function that takes an event (of subtype of EventBase) and no other parameters.
36+
37+
Typically used for event handlers that have already had parameters injected.
38+
"""
39+
40+
41+
3142
# TODO: Fix this up to push to a log in the apropos directory and filename.
3243
logger = getLogger("streamdeck.manager")
3344

3445

46+
def is_bindable_handler(handler: EventHandlerFunc[EventModel_contra, InjectableParams]) -> TypeGuard[BindableEventHandlerFunc[EventModel_contra]]:
47+
"""Check if the handler is prebound with the `command_sender` parameter."""
48+
# Check dynamically if the `command_sender`'s name is in the handler's arguments.
49+
return "command_sender" in inspect.signature(handler).parameters
50+
51+
52+
def is_not_bindable_handler(handler: EventHandlerFunc[EventModel_contra, InjectableParams]) -> TypeGuard[BoundEventHandlerFunc[EventModel_contra]]:
53+
"""Check if the handler only accepts the event_data parameter.
54+
55+
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.
56+
"""
57+
handler_params = inspect.signature(handler).parameters
58+
return len(handler_params) == 1 and "event_data" in handler_params
59+
3560

3661
class PluginManager:
3762
"""Manages plugin actions and communicates with a WebSocket server to handle events."""
@@ -105,7 +130,7 @@ def register_event_listener(self, listener: EventListener) -> None:
105130
for event_model in listener.event_models:
106131
self._event_adapter.add_model(event_model)
107132

108-
def _inject_command_sender(self, handler: EventHandlerFunc[TEvent_contra], command_sender: StreamDeckCommandSender) -> EventHandlerBasicFunc[TEvent_contra]:
133+
def _inject_command_sender(self, handler: EventHandlerFunc[EventModel_contra, InjectableParams], command_sender: StreamDeckCommandSender) -> BoundEventHandlerFunc[EventModel_contra]:
109134
"""Inject command_sender into handler if it accepts it as a parameter.
110135
111136
Args:
@@ -116,8 +141,13 @@ def _inject_command_sender(self, handler: EventHandlerFunc[TEvent_contra], comma
116141
The handler with command_sender injected if needed
117142
"""
118143
if is_bindable_handler(handler):
144+
# If the handler accepts command_sender, inject it and return the handler.
119145
return functools.partial(handler, command_sender=command_sender)
120146

147+
if not is_not_bindable_handler(handler):
148+
# If the handler is neither bindable nor not bindable, raise an error.
149+
raise TypeError(f"Invalid event handler function signature: {handler}") # noqa: TRY003, EM102
150+
121151
return handler
122152

123153
def _stream_event_data(self) -> Generator[EventBase, None, None]:

streamdeck/types.py

Lines changed: 0 additions & 66 deletions
This file was deleted.

tests/data/dummy_event_listener.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@
1212
from typing import Any, ClassVar
1313

1414

15-
class DummyEvent(EventBase):
15+
class DummyEvent(EventBase["dummy"]):
1616
"""A dummy event for testing purposes."""
17-
event: Literal["dummy"] # type: ignore[assignment]
1817
something: int
1918

2019

tests/models/events/__init__.py

Whitespace-only changes.

tests/plugin_manager/test_command_sender_binding.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,14 @@
1111
if TYPE_CHECKING:
1212
from functools import partial
1313

14+
from streamdeck.actions import EventHandlerFunc
1415
from streamdeck.command_sender import StreamDeckCommandSender
1516
from streamdeck.manager import PluginManager
1617
from streamdeck.models import events
17-
from streamdeck.types import (
18-
EventHandlerBasicFunc,
19-
EventHandlerFunc,
20-
)
2118

2219

2320

24-
def create_event_handler(include_command_sender_param: bool = False) -> EventHandlerFunc[events.EventBase]:
21+
def create_event_handler(include_command_sender_param: bool = False) -> EventHandlerFunc:
2522
"""Create a dummy event handler function that matches the EventHandlerFunc TypeAlias.
2623
2724
Args:
@@ -45,7 +42,7 @@ def dummy_handler_with_cmd_sender(event_data: events.EventBase, command_sender:
4542
@pytest.fixture(params=[True, False])
4643
def mock_event_handler(request: pytest.FixtureRequest) -> Mock:
4744
include_command_sender_param: bool = request.param
48-
dummy_handler: EventHandlerFunc[events.EventBase] = create_event_handler(include_command_sender_param)
45+
dummy_handler: EventHandlerFunc = create_event_handler(include_command_sender_param)
4946

5047
return create_autospec(dummy_handler, spec_set=True)
5148

@@ -57,7 +54,7 @@ def test_inject_command_sender_func(
5754
) -> None:
5855
"""Test that the command_sender is injected into the handler."""
5956
mock_command_sender = Mock()
60-
result_handler: EventHandlerFunc [events.EventBase]= plugin_manager._inject_command_sender(mock_event_handler, mock_command_sender)
57+
result_handler: EventHandlerFunc = plugin_manager._inject_command_sender(mock_event_handler, mock_command_sender)
6158

6259
resulting_handler_params = inspect.signature(result_handler).parameters
6360

@@ -78,7 +75,7 @@ def test_inject_command_sender_func(
7875

7976
@pytest.mark.usefixtures("patch_websocket_client")
8077
def test_run_manager_events_handled_with_correct_params(
81-
mock_event_listener_manager_with_fake_events: tuple[Mock, list[events.EventBase[LiteralStrGenericAlias]]],
78+
mock_event_listener_manager_with_fake_events: tuple[Mock, list[events.EventBase]],
8279
plugin_manager: PluginManager,
8380
mock_command_sender: Mock,
8481
) -> None:

0 commit comments

Comments
 (0)