Skip to content

Commit 3605057

Browse files
committed
make an EventHandlerType protocol
1 parent d150f10 commit 3605057

File tree

7 files changed

+314
-190
lines changed

7 files changed

+314
-190
lines changed

src/idom/core/events.py

Lines changed: 172 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,21 @@
22
Events
33
"""
44

5+
from __future__ import annotations
6+
57
import asyncio
6-
from typing import (
7-
Any,
8-
Callable,
9-
Coroutine,
10-
Dict,
11-
Iterator,
12-
List,
13-
Mapping,
14-
Optional,
15-
Union,
16-
)
17-
from uuid import uuid4
8+
from typing import Any, Callable, Iterator, List, Mapping, Optional, Sequence
189

1910
from anyio import create_task_group
2011

21-
22-
EventsMapping = Union[Dict[str, Union["Callable[..., Any]", "EventHandler"]], "Events"]
12+
from idom.core.proto import EventHandlerDict, EventHandlerFunc, EventHandlerType
2313

2414

2515
def event(
26-
function: Optional[Callable[..., Any]] = None,
2716
stop_propagation: bool = False,
2817
prevent_default: bool = False,
29-
) -> Union["EventHandler", Callable[[Callable[..., Any]], "EventHandler"]]:
30-
"""Create an event handler function with extra functionality.
18+
) -> Callable[[Callable[..., Any]], EventHandler]:
19+
"""A decorator for constructing an :class:`EventHandler`.
3120
3221
While you're always free to add callbacks by assigning them to an element's attributes
3322
@@ -39,23 +28,78 @@ def event(
3928
from taking place, or stoping the event from propagating up the DOM. This decorator
4029
allows you to add that functionality to your callbacks.
4130
31+
.. code-block:: python
32+
33+
@event(stop_propagation=True, prevent_default=True)
34+
def my_callback(*data):
35+
...
36+
37+
element = idom.html.button({"onClick": my_callback})
38+
4239
Parameters:
4340
function:
44-
A callback responsible for handling the event.
41+
A function or coroutine responsible for handling the event.
4542
stop_propagation:
4643
Block the event from propagating further up the DOM.
4744
prevent_default:
4845
Stops the default actional associate with the event from taking place.
4946
"""
50-
handler = EventHandler(stop_propagation, prevent_default)
51-
if function is not None:
52-
handler.add(function)
53-
return handler
54-
else:
55-
return handler.add
5647

48+
def setup(function: Callable[..., Any]) -> EventHandler:
49+
return EventHandler(
50+
to_event_handler_function(function),
51+
stop_propagation,
52+
prevent_default,
53+
)
54+
55+
return setup
56+
57+
58+
class EventHandler:
59+
"""Turn a function or coroutine into an event handler
60+
61+
Parameters:
62+
function:
63+
The function or coroutine which handles the event.
64+
stop_propagation:
65+
Block the event from propagating further up the DOM.
66+
prevent_default:
67+
Stops the default action associate with the event from taking place.
68+
target:
69+
A unique identifier for this event handler (auto-generated by default)
70+
"""
5771

58-
class Events(Mapping[str, "EventHandler"]):
72+
__slots__ = (
73+
"__weakref__",
74+
"function",
75+
"prevent_default",
76+
"stop_propagation",
77+
"target",
78+
)
79+
80+
def __init__(
81+
self,
82+
function: EventHandlerFunc,
83+
stop_propagation: bool = False,
84+
prevent_default: bool = False,
85+
target: Optional[str] = None,
86+
) -> None:
87+
self.function = function
88+
self.prevent_default = prevent_default
89+
self.stop_propagation = stop_propagation
90+
self.target = target
91+
92+
def __repr__(self) -> str:
93+
public_names = [name for name in self.__slots__ if not name.startswith("_")]
94+
items = ", ".join([f"{n}={getattr(self, n)!r}" for n in public_names])
95+
return f"{type(self).__name__}({items})"
96+
97+
98+
async def _no_op(data: List[Any]) -> None:
99+
return None
100+
101+
102+
class Events(Mapping[str, EventHandler]):
59103
"""A container for event handlers.
60104
61105
Assign this object to the ``"eventHandlers"`` field of an element model.
@@ -64,10 +108,13 @@ class Events(Mapping[str, "EventHandler"]):
64108
__slots__ = "_handlers"
65109

66110
def __init__(self) -> None:
67-
self._handlers: Dict[str, EventHandler] = {}
111+
self._handlers: EventHandlerDict = {}
68112

69113
def on(
70-
self, event: str, stop_propagation: bool = False, prevent_default: bool = False
114+
self,
115+
event: str,
116+
stop_propagation: Optional[bool] = None,
117+
prevent_default: Optional[bool] = None,
71118
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
72119
"""A decorator for adding an event handler.
73120
@@ -105,13 +152,46 @@ def handler(event):
105152
event = "on" + event[:1].upper() + event[1:]
106153

107154
if event not in self._handlers:
108-
handler = EventHandler(stop_propagation, prevent_default)
109-
self._handlers[event] = handler
110-
else:
111-
handler = self._handlers[event]
155+
# do this so it's possible to stop event propagation or default behavior
156+
# without making the user have to pass a no op event handler themselves
157+
self._handlers[event] = EventHandler(
158+
_no_op,
159+
stop_propagation,
160+
prevent_default,
161+
)
112162

113163
def setup(function: Callable[..., Any]) -> Callable[..., Any]:
114-
handler.add(function)
164+
old_handler = self._handlers[event]
165+
166+
if old_handler.function is _no_op:
167+
return EventHandler(
168+
to_event_handler_function(function),
169+
bool(stop_propagation),
170+
bool(prevent_default),
171+
)
172+
173+
new_stop_propagation = (
174+
old_handler.stop_propagation
175+
if stop_propagation is None
176+
else stop_propagation
177+
)
178+
new_prevent_default = (
179+
old_handler.prevent_default
180+
if prevent_default is None
181+
else prevent_default
182+
)
183+
184+
self._handlers[event] = merge_event_handlers(
185+
[
186+
old_handler,
187+
EventHandler(
188+
to_event_handler_function(function),
189+
new_stop_propagation,
190+
new_prevent_default,
191+
),
192+
]
193+
)
194+
115195
return function
116196

117197
return setup
@@ -125,91 +205,79 @@ def __len__(self) -> int:
125205
def __iter__(self) -> Iterator[str]:
126206
return iter(self._handlers)
127207

128-
def __getitem__(self, key: str) -> "EventHandler":
208+
def __getitem__(self, key: str) -> EventHandler:
129209
return self._handlers[key]
130210

131211
def __repr__(self) -> str: # pragma: no cover
132212
return repr(self._handlers)
133213

134214

135-
class EventHandler:
136-
"""An object which defines an event handler.
137-
138-
The event handler object acts like a coroutine when called.
215+
def to_event_handler_function(function: Callable[..., Any]) -> EventHandlerFunc:
216+
"""Make a :data:`~idom.core.proto.EventHandlerFunc` from a function or coroutine
139217
140218
Parameters:
141-
stop_propagation:
142-
Block the event from propagating further up the DOM.
143-
prevent_default:
144-
Stops the default action associate with the event from taking place.
219+
function:
220+
A function or coroutine accepting a number of positional arguments.
145221
"""
222+
if asyncio.iscoroutinefunction(function):
223+
return lambda data: function(*data)
224+
else:
146225

147-
__slots__ = (
148-
"__weakref__",
149-
"_coro_handlers",
150-
"_func_handlers",
151-
"prevent_default",
152-
"stop_propagation",
153-
"target",
154-
)
226+
async def wrapper(data: List[Any]) -> None:
227+
return function(*data)
155228

156-
def __init__(
157-
self,
158-
stop_propagation: bool = False,
159-
prevent_default: bool = False,
160-
) -> None:
161-
self._coro_handlers: List[Callable[..., Coroutine[Any, Any, Any]]] = []
162-
self._func_handlers: List[Callable[..., Any]] = []
163-
self.prevent_default = prevent_default
164-
self.stop_propagation = stop_propagation
165-
self.target = uuid4().hex
229+
return wrapper
166230

167-
def add(self, function: Callable[..., Any]) -> "EventHandler":
168-
"""Add a callback function or coroutine to the event handler.
169231

170-
Parameters:
171-
function:
172-
The event handler function accepting parameters sent by the client.
173-
Typically this is a single ``event`` parameter that is a dictionary.
174-
"""
175-
if asyncio.iscoroutinefunction(function):
176-
self._coro_handlers.append(function)
177-
else:
178-
self._func_handlers.append(function)
179-
return self
232+
def merge_event_handlers(event_handlers: Sequence[EventHandlerType]) -> EventHandler:
233+
"""Merge multiple event handlers into one
180234
181-
def remove(self, function: Callable[..., Any]) -> None:
182-
"""Remove the given function or coroutine from this event handler.
235+
Raises a ValueError if any handlers have conflicting
236+
:attr:`~idom.core.proto.EventHandlerType.stop_propagation` or
237+
:attr:`~idom.core.proto.EventHandlerType.prevent_default` attributes.
238+
"""
239+
if not event_handlers:
240+
raise ValueError("No event handlers to merge")
241+
elif len(event_handlers) == 1:
242+
return event_handlers[0]
243+
244+
first_handler = event_handlers[0]
245+
246+
stop_propagation = first_handler.stop_propagation
247+
prevent_default = first_handler.prevent_default
248+
target = first_handler.target
249+
250+
for handler in event_handlers:
251+
if (
252+
handler.stop_propagation != stop_propagation
253+
or handler.prevent_default != prevent_default
254+
or handler.target != target
255+
):
256+
raise ValueError(
257+
"Cannot merge handlers - "
258+
"'stop_propagation', 'prevent_default' or 'target' mistmatch."
259+
)
260+
261+
return EventHandler(
262+
merge_event_handler_funcs([h.function for h in event_handlers]),
263+
stop_propagation,
264+
prevent_default,
265+
target,
266+
)
183267

184-
Raises:
185-
ValueError: if not found
186-
"""
187-
if asyncio.iscoroutinefunction(function):
188-
self._coro_handlers.remove(function)
189-
else:
190-
self._func_handlers.remove(function)
191-
192-
def clear(self) -> None:
193-
"""Remove all functions and coroutines from this event handler"""
194-
self._coro_handlers.clear()
195-
self._func_handlers.clear()
196-
197-
async def __call__(self, data: List[Any]) -> Any:
198-
"""Trigger all callbacks in the event handler."""
199-
if self._coro_handlers:
200-
async with create_task_group() as group:
201-
for handler in self._coro_handlers:
202-
group.start_soon(handler, *data)
203-
for handler in self._func_handlers:
204-
handler(*data)
205-
206-
def __contains__(self, function: Any) -> bool:
207-
if asyncio.iscoroutinefunction(function):
208-
return function in self._coro_handlers
209-
else:
210-
return function in self._func_handlers
211268

212-
def __repr__(self) -> str:
213-
public_names = [name for name in self.__slots__ if not name.startswith("_")]
214-
items = ", ".join([f"{n}={getattr(self, n)!r}" for n in public_names])
215-
return f"{type(self).__name__}({items})"
269+
def merge_event_handler_funcs(
270+
functions: Sequence[EventHandlerFunc],
271+
) -> EventHandlerFunc:
272+
"""Make one event handler function from many"""
273+
if not functions:
274+
raise ValueError("No handler functions to merge")
275+
elif len(functions) == 1:
276+
return functions[0]
277+
278+
async def await_all_event_handlers(data: List[Any]) -> None:
279+
async with create_task_group() as group:
280+
for func in functions:
281+
group.start_soon(func, data)
282+
283+
return await_all_event_handlers

0 commit comments

Comments
 (0)