Added support for returning values from event handlers and to pass arbitrary arguments to them when emitting events

This commit is contained in:
nocturn9x 2022-01-07 18:23:24 +01:00
parent 91f1e6b5a0
commit 070cd2bcd8
4 changed files with 96 additions and 36 deletions

View File

@ -11,7 +11,7 @@
import functools import functools
import threading import threading
from asyncevents import events, errors from asyncevents import events, errors
from typing import Any, Callable, Coroutine, Optional from typing import Any, Callable, Coroutine, Optional, List
from asyncevents.constants import ExceptionHandling, ExecutionMode, UnknownEventHandling from asyncevents.constants import ExceptionHandling, ExecutionMode, UnknownEventHandling
# Thread-local namespace for storing the currently # Thread-local namespace for storing the currently
@ -51,20 +51,20 @@ def set_current_emitter(emitter: AsyncEventEmitter):
local_storage.emitter = emitter local_storage.emitter = emitter
async def wait(event: Optional[str] = None): async def wait(event: Optional[str] = None) -> List[Any]:
""" """
Shorthand for get_current_emitter().wait() Shorthand for get_current_emitter().wait()
""" """
await get_current_emitter().wait(event) return await get_current_emitter().wait(event)
async def emit(event: str, block: bool = True): async def emit(event: str, block: bool = True, *args, **kwargs) -> List[Any]:
""" """
Shorthand for get_current_emitter().emit(event, block) Shorthand for get_current_emitter().emit(event, block)
""" """
await get_current_emitter().emit(event, block) return await get_current_emitter().emit(event, block, *args, **kwargs)
async def exists(event: str): async def exists(event: str):

View File

@ -83,12 +83,12 @@ class AsyncEventEmitter:
# It's a coroutine! Call it # It's a coroutine! Call it
await self.on_unknown_event(self, event) await self.on_unknown_event(self, event)
async def _handle_errors_in_awaitable(self, event: str, obj: Awaitable): async def _handle_errors_in_awaitable(self, event: str, obj: Awaitable) -> Any:
# Thanks to asyncio's *utterly amazing* (HUGE sarcasm there) # Thanks to asyncio's *utterly amazing* (HUGE sarcasm there)
# exception handling, we have to make this wrapper so we can # exception handling, we have to make this wrapper so we can
# catch errors on a per-handler basis # catch errors on a per-handler basis
try: try:
await obj return await obj
except Exception as e: except Exception as e:
if (event, obj) in self._tasks: if (event, obj) in self._tasks:
obj: asyncio.Task # Silences PyCharm's warnings obj: asyncio.Task # Silences PyCharm's warnings
@ -103,12 +103,12 @@ class AsyncEventEmitter:
# Implementations for emit() # Implementations for emit()
async def _emit_nowait(self, event: str): async def _emit_nowait(self, event: str, *args, **kwargs):
# This implementation of emit() returns immediately # This implementation of emit() returns immediately
# and runs the handlers in the background # and runs the handlers in the background
await self._check_event(event) await self._check_event(event)
temp: List[Tuple[int, float, Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]], bool]] = [] temp: List[Tuple[int, float, Callable[["AsyncEventEmitter", str, ...], Coroutine[Any, Any, Any]], bool]] = []
t: Tuple[int, float, Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]], bool] t: Tuple[int, float, Callable[["AsyncEventEmitter", str, ...], Coroutine[Any, Any, Any]], bool]
while self.handlers[event]: while self.handlers[event]:
# We use heappop because we want the first # We use heappop because we want the first
# by priority and the heap queue only has # by priority and the heap queue only has
@ -118,20 +118,21 @@ class AsyncEventEmitter:
if t[-1]: if t[-1]:
# It won't be re-scheduled # It won't be re-scheduled
temp.pop() temp.pop()
task = asyncio.create_task(t[-2](self, event)) task = asyncio.create_task(t[-2](self, event, *args, **kwargs))
self._tasks.append((event, asyncio.create_task(self._handle_errors_in_awaitable(event, task)))) self._tasks.append((event, asyncio.create_task(self._handle_errors_in_awaitable(event, task))))
# We push back the elements # We push back the elements
for t in temp: for t in temp:
heappush(self.handlers[event], t) heappush(self.handlers[event], t)
async def _emit_await(self, event: str): async def _emit_await(self, event: str, *args, **kwargs) -> List[Any]:
# This implementation of emit() returns only after # This implementation of emit() returns only after
# all handlers have finished executing # all handlers have finished executing
await self._check_event(event) await self._check_event(event)
result = []
temp: List[ temp: List[
Tuple[int, float, Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]], bool] Tuple[int, float, Callable[["AsyncEventEmitter", str, ...], Coroutine[Any, Any, Any]], bool]
] = self.handlers[event].copy() ] = self.handlers[event].copy()
t: Tuple[int, float, Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]], bool] t: Tuple[int, float, Callable[["AsyncEventEmitter", str, ...], Coroutine[Any, Any, Any]], bool]
while temp: while temp:
# We use heappop because we want the first # We use heappop because we want the first
# by priority and the heap queue only has # by priority and the heap queue only has
@ -139,7 +140,8 @@ class AsyncEventEmitter:
t = heappop(temp) t = heappop(temp)
if t[-1]: if t[-1]:
self.unregister_handler(event, t[-2]) self.unregister_handler(event, t[-2])
await self._handle_errors_in_awaitable(event, t[-2](self, event)) result.append(await self._handle_errors_in_awaitable(event, t[-2](self, event, *args, **kwargs)))
return result
def __init__( def __init__(
self, self,
@ -148,7 +150,7 @@ class AsyncEventEmitter:
on_error: ExceptionHandling on_error: ExceptionHandling
| Callable[["AsyncEventEmitter", Exception, str], Coroutine[Any, Any, Any]] = ExceptionHandling.PROPAGATE, | Callable[["AsyncEventEmitter", Exception, str], Coroutine[Any, Any, Any]] = ExceptionHandling.PROPAGATE,
on_unknown_event: UnknownEventHandling on_unknown_event: UnknownEventHandling
| Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]] = UnknownEventHandling.IGNORE, | Callable[["AsyncEventEmitter", str, ...], Coroutine[Any, Any, Any]] = UnknownEventHandling.IGNORE,
mode: ExecutionMode = ExecutionMode.PAUSE, mode: ExecutionMode = ExecutionMode.PAUSE,
): ):
""" """
@ -179,11 +181,11 @@ class AsyncEventEmitter:
# handler is to be deleted after it fires the first # handler is to be deleted after it fires the first
# time (aka 'oneshot') # time (aka 'oneshot')
self.handlers: Dict[ self.handlers: Dict[
str, List[Tuple[int, float, Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]], bool]] str, List[Tuple[int, float, Callable[["AsyncEventEmitter", str, ...], Coroutine[Any, Any, Any]], bool]]
] = defaultdict(list) ] = defaultdict(list)
@property @property
def on_error(self) -> ExceptionHandling | Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]]: def on_error(self) -> ExceptionHandling | Callable[["AsyncEventEmitter", str, ...], Coroutine[Any, Any, Any]]:
""" """
Property getter for on_error Property getter for on_error
""" """
@ -191,7 +193,7 @@ class AsyncEventEmitter:
return self._on_error return self._on_error
@on_error.setter @on_error.setter
def on_error(self, on_error: ExceptionHandling | Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]]): def on_error(self, on_error: ExceptionHandling | Callable[["AsyncEventEmitter", str, ...], Coroutine[Any, Any, Any]]):
""" """
Property setter for on_error Property setter for on_error
@ -222,7 +224,7 @@ class AsyncEventEmitter:
self._on_error = on_error self._on_error = on_error
@property @property
def on_unknown_event(self) -> UnknownEventHandling | Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]]: def on_unknown_event(self) -> UnknownEventHandling | Callable[["AsyncEventEmitter", str, ...], Coroutine[Any, Any, Any]]:
""" """
Property getter for on_unknown_event Property getter for on_unknown_event
""" """
@ -233,7 +235,7 @@ class AsyncEventEmitter:
def on_unknown_event( def on_unknown_event(
self, self,
on_unknown_event: UnknownEventHandling on_unknown_event: UnknownEventHandling
| Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]] = UnknownEventHandling.IGNORE, | Callable[["AsyncEventEmitter", str, ...], Coroutine[Any, Any, Any]] = UnknownEventHandling.IGNORE,
): ):
""" """
Property setter for on_unknown_event Property setter for on_unknown_event
@ -314,7 +316,7 @@ class AsyncEventEmitter:
def register_event( def register_event(
self, self,
event: str, event: str,
handler: Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]], handler: Callable[["AsyncEventEmitter", str, ...], Coroutine[Any, Any, Any]],
priority: int = 0, priority: int = 0,
oneshot: bool = False, oneshot: bool = False,
): ):
@ -329,7 +331,7 @@ class AsyncEventEmitter:
:type event: str :type event: str
:param handler: A coroutine function to be called :param handler: A coroutine function to be called
when the event is generated when the event is generated
:type handler: Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]] :type handler: Callable[["AsyncEventEmitter", str, ...], Coroutine[Any, Any, Any]]
:param priority: The handler's execution priority, :param priority: The handler's execution priority,
defaults to 0 (lower priority means earlier defaults to 0 (lower priority means earlier
execution!) execution!)
@ -357,8 +359,8 @@ class AsyncEventEmitter:
self.handlers.pop(event, None) self.handlers.pop(event, None)
def _get( def _get(
self, event: str, handler: Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]] self, event: str, handler: Callable[["AsyncEventEmitter", str, ...], Coroutine[Any, Any, Any]]
) -> None | bool | Tuple[int, float, Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]], bool]: ) -> None | bool | Tuple[int, float, Callable[["AsyncEventEmitter", str, ...], Coroutine[Any, Any, Any]], bool]:
""" """
Returns the tuple of (priority, date, corofunc, oneshot) representing the Returns the tuple of (priority, date, corofunc, oneshot) representing the
given handler. Only the first matching entry is returned. False is returned given handler. Only the first matching entry is returned. False is returned
@ -370,7 +372,7 @@ class AsyncEventEmitter:
:type event: str :type event: str
:param handler: A coroutine function to be called :param handler: A coroutine function to be called
when the event is generated when the event is generated
:type handler: Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]] :type handler: Callable[["AsyncEventEmitter", str, ...], Coroutine[Any, Any, Any]]
:raises: :raises:
UnknownEvent: If self.on_unknown_error == UnknownEventHandling.ERROR UnknownEvent: If self.on_unknown_error == UnknownEventHandling.ERROR
:returns: The tuple of (priority, date, coro) representing the :returns: The tuple of (priority, date, coro) representing the
@ -387,7 +389,7 @@ class AsyncEventEmitter:
def unregister_handler( def unregister_handler(
self, self,
event: str, event: str,
handler: Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]], handler: Callable[["AsyncEventEmitter", str, ...], Coroutine[Any, Any, Any]],
remove_all: bool = False, remove_all: bool = False,
): ):
""" """
@ -402,7 +404,7 @@ class AsyncEventEmitter:
:param event: The event name :param event: The event name
:type event: str :type event: str
:param handler: The coroutine function to be unregistered :param handler: The coroutine function to be unregistered
:type handler: Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]] :type handler: Callable[["AsyncEventEmitter", str, ...], Coroutine[Any, Any, Any]]
:param remove_all: If True, all occurrences of the :param remove_all: If True, all occurrences of the
given handler are removed, otherwise only the first given handler are removed, otherwise only the first
one is unregistered one is unregistered
@ -422,7 +424,7 @@ class AsyncEventEmitter:
# We maintain the heap queue invariant # We maintain the heap queue invariant
heapify(self.handlers[event]) heapify(self.handlers[event])
async def wait(self, event: Optional[str] = None): async def wait(self, event: Optional[str] = None) -> List[Any]:
""" """
Waits until all the event handlers for the given Waits until all the event handlers for the given
event have finished executing. When the given event event have finished executing. When the given event
@ -430,19 +432,29 @@ class AsyncEventEmitter:
events to terminate. This method is a no-op when the events to terminate. This method is a no-op when the
emitter is configured with anything other than emitter is configured with anything other than
ExecutionMode.NOWAIT or if emit() hasn't been called ExecutionMode.NOWAIT or if emit() hasn't been called
with block=False with block=False. Returns a list of all return values
from the event handlers
""" """
if not event: if not event:
await asyncio.gather(*[t[1] for t in self._tasks]) result = [e for e in await asyncio.gather(*[t[1] for t in self._tasks])]
self._tasks = [] self._tasks = []
return result
else: else:
await self._check_event(event) await self._check_event(event)
await asyncio.gather(*[t[1] for t in self._tasks if t[0] == event]) result = [e for e in await asyncio.gather(*[t[1] for t in self._tasks if t[0] == event])]
for t in self._tasks:
if t[0] == event:
self._tasks.remove(t)
return result
async def emit(self, event: str, block: bool = True): async def emit(self, event: str, block: bool = True, *args, **kwargs) -> List[Any]:
""" """
Emits an event Emits an event. Any extra positional and keyword arguments besides
the event name and the "block" boolean are passed over to the
event handlers themselves. Returns the values of the event
handlers in a list when using blocking mode or an empty
list otherwise
:param event: The event to trigger. Note that, :param event: The event to trigger. Note that,
depending on the configuration, unknown events depending on the configuration, unknown events
@ -461,8 +473,10 @@ class AsyncEventEmitter:
mode = self._mode mode = self._mode
if block: if block:
self._mode = ExecutionMode.PAUSE self._mode = ExecutionMode.PAUSE
await self._emit_await(event) result = await self._emit_await(event, *args, **kwargs)
else: else:
self._mode = ExecutionMode.NOWAIT self._mode = ExecutionMode.NOWAIT
await self._emit_nowait(event) await self._emit_nowait(event, *args, **kwargs)
result = []
self._mode = mode self._mode = mode
return result

17
tests/arguments.py Normal file
View File

@ -0,0 +1,17 @@
import asyncio
from asyncevents import on_event, emit
@on_event("hello")
async def hello(_, event: str, n: int):
print(f"Hello {event!r}! The number is {n}!")
async def main():
print("Firing blocking event 'hello'")
await emit("hello", True, 5)
print("Handlers for event 'hello' have exited")
if __name__ == "__main__":
asyncio.run(main())

29
tests/return_values.py Normal file
View File

@ -0,0 +1,29 @@
import asyncio
from asyncevents import on_event, emit
@on_event("hello")
async def hello(_, event: str):
print(f"Hello {event!r}!")
return 42
@on_event("hello")
async def owo(_, event: str):
print(f"owo {event!r}!")
return 1
@on_event("hello")
async def hi(_, event: str):
print(f"Hello {event!r}!")
async def main():
print("Firing blocking event 'hello'")
assert await emit("hello") == [42, 1, None]
print("Handlers for event 'hello' have exited")
if __name__ == "__main__":
asyncio.run(main())