From 070cd2bcd8d39293fc0c9a00681a5e2826d79112 Mon Sep 17 00:00:00 2001 From: nocturn9x Date: Fri, 7 Jan 2022 18:23:24 +0100 Subject: [PATCH] Added support for returning values from event handlers and to pass arbitrary arguments to them when emitting events --- asyncevents/__init__.py | 10 +++--- asyncevents/events.py | 76 ++++++++++++++++++++++++----------------- tests/arguments.py | 17 +++++++++ tests/return_values.py | 29 ++++++++++++++++ 4 files changed, 96 insertions(+), 36 deletions(-) create mode 100644 tests/arguments.py create mode 100644 tests/return_values.py diff --git a/asyncevents/__init__.py b/asyncevents/__init__.py index 2c9156f..2575826 100644 --- a/asyncevents/__init__.py +++ b/asyncevents/__init__.py @@ -11,7 +11,7 @@ import functools import threading 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 # Thread-local namespace for storing the currently @@ -51,20 +51,20 @@ def set_current_emitter(emitter: AsyncEventEmitter): 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() """ - 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) """ - await get_current_emitter().emit(event, block) + return await get_current_emitter().emit(event, block, *args, **kwargs) async def exists(event: str): diff --git a/asyncevents/events.py b/asyncevents/events.py index aa5d621..b4c1184 100644 --- a/asyncevents/events.py +++ b/asyncevents/events.py @@ -83,12 +83,12 @@ class AsyncEventEmitter: # It's a coroutine! Call it 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) # exception handling, we have to make this wrapper so we can # catch errors on a per-handler basis try: - await obj + return await obj except Exception as e: if (event, obj) in self._tasks: obj: asyncio.Task # Silences PyCharm's warnings @@ -103,12 +103,12 @@ class AsyncEventEmitter: # 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 # and runs the handlers in the background await self._check_event(event) - 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] + 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] while self.handlers[event]: # We use heappop because we want the first # by priority and the heap queue only has @@ -118,20 +118,21 @@ class AsyncEventEmitter: if t[-1]: # It won't be re-scheduled 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)))) # We push back the elements for t in temp: 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 # all handlers have finished executing await self._check_event(event) + result = [] 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() - 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: # We use heappop because we want the first # by priority and the heap queue only has @@ -139,7 +140,8 @@ class AsyncEventEmitter: t = heappop(temp) if t[-1]: 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__( self, @@ -148,7 +150,7 @@ class AsyncEventEmitter: on_error: ExceptionHandling | Callable[["AsyncEventEmitter", Exception, str], Coroutine[Any, Any, Any]] = ExceptionHandling.PROPAGATE, 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, ): """ @@ -179,11 +181,11 @@ class AsyncEventEmitter: # handler is to be deleted after it fires the first # time (aka 'oneshot') 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) @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 """ @@ -191,7 +193,7 @@ class AsyncEventEmitter: return self._on_error @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 @@ -222,7 +224,7 @@ class AsyncEventEmitter: self._on_error = on_error @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 """ @@ -233,7 +235,7 @@ class AsyncEventEmitter: def on_unknown_event( self, 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 @@ -314,7 +316,7 @@ class AsyncEventEmitter: def register_event( self, event: str, - handler: Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]], + handler: Callable[["AsyncEventEmitter", str, ...], Coroutine[Any, Any, Any]], priority: int = 0, oneshot: bool = False, ): @@ -329,7 +331,7 @@ class AsyncEventEmitter: :type event: str :param handler: A coroutine function to be called 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, defaults to 0 (lower priority means earlier execution!) @@ -357,8 +359,8 @@ class AsyncEventEmitter: self.handlers.pop(event, None) def _get( - self, event: str, handler: Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]] - ) -> None | bool | Tuple[int, float, Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]], bool]: + self, event: str, handler: Callable[["AsyncEventEmitter", str, ...], Coroutine[Any, Any, Any]] + ) -> None | bool | Tuple[int, float, Callable[["AsyncEventEmitter", str, ...], Coroutine[Any, Any, Any]], bool]: """ Returns the tuple of (priority, date, corofunc, oneshot) representing the given handler. Only the first matching entry is returned. False is returned @@ -370,7 +372,7 @@ class AsyncEventEmitter: :type event: str :param handler: A coroutine function to be called when the event is generated - :type handler: Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]] + :type handler: Callable[["AsyncEventEmitter", str, ...], Coroutine[Any, Any, Any]] :raises: UnknownEvent: If self.on_unknown_error == UnknownEventHandling.ERROR :returns: The tuple of (priority, date, coro) representing the @@ -387,7 +389,7 @@ class AsyncEventEmitter: def unregister_handler( self, event: str, - handler: Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]], + handler: Callable[["AsyncEventEmitter", str, ...], Coroutine[Any, Any, Any]], remove_all: bool = False, ): """ @@ -402,7 +404,7 @@ class AsyncEventEmitter: :param event: The event name :type event: str :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 given handler are removed, otherwise only the first one is unregistered @@ -422,7 +424,7 @@ class AsyncEventEmitter: # We maintain the heap queue invariant 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 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 emitter is configured with anything other than 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: - 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 = [] + return result else: 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, depending on the configuration, unknown events @@ -461,8 +473,10 @@ class AsyncEventEmitter: mode = self._mode if block: self._mode = ExecutionMode.PAUSE - await self._emit_await(event) + result = await self._emit_await(event, *args, **kwargs) else: self._mode = ExecutionMode.NOWAIT - await self._emit_nowait(event) + await self._emit_nowait(event, *args, **kwargs) + result = [] self._mode = mode + return result diff --git a/tests/arguments.py b/tests/arguments.py new file mode 100644 index 0000000..df22aef --- /dev/null +++ b/tests/arguments.py @@ -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()) diff --git a/tests/return_values.py b/tests/return_values.py new file mode 100644 index 0000000..79216c7 --- /dev/null +++ b/tests/return_values.py @@ -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()) \ No newline at end of file