diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/asyncevents.iml b/.idea/asyncevents.iml
new file mode 100644
index 0000000..74d515a
--- /dev/null
+++ b/.idea/asyncevents.iml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..b85da15
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..f97b381
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..2ccb9b1
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..46b7bdb
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index 261eeb9..a7b8b5e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -184,18 +184,4 @@
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+ identification within third-party archives.
\ No newline at end of file
diff --git a/README.md b/README.md
index e4f44ae..b82c7a5 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,84 @@
-# asyncevents
-asyncevents is a small library to help developers perform asynchronous event handling in modern Python code
+# asyncevents - Asynchronous event handling for modern Python
+
+asyncevents is a small library to help developers perform asynchronous event handling in modern Python code.
+
+## Features
+
+- Priority queueing to allow for deterministic event triggering (with a catch, see below)
+- Built-in exception handling (optional)
+- The public API is fully type hinted for those sweet, sweet editor suggestions
+- Public API is fully documented (and some private stuff too): you could write this from scratch in a couple of hours (like I did)
+- Very small (~200 CLOC), although it can't fit on a postcard
+
+
+__Note__: Deterministic event handling can only occur in blocking mode, i.e. when a call to `emit()` blocks until
+all event handlers have run. If the non-blocking mode is used, handlers are started according to their priority,
+but there's no telling on how they will be further scheduled to run and that depends entirely on the underlying
+asyncio event loop
+
+## Limitations
+
+- Only compatible with asyncio due to the fact that other libraries such as trio and curio have wildly different design
+ goals (structured concurrency, separation of environments, etc.), which makes implementing some functionality
+ (i.e. the `wait()` method) tricky, if not outright impossible. Also those libraries, especially trio, already have
+ decent machinery to perform roughly what asyncevent does
+- Does not support using any other loop than the currently running one because of some subtleties of modern asyncio
+ wrappers like `asyncio.run()` which creates its own event loop internally (_Thanks, asyncio_)
+- Exceptions are kinda finicky in non-blocking mode due to how `asyncio.gather` works: only the first exception
+ in a group of handlers is properly raised and log messages might get doubled. Also, exception logging and propagation
+ is delayed until you `await wait("some_event")` so be careful
+## Why?
+
+This library exists because the current alternatives either suck, lack features or are inspired by other languages'
+implementations of events like C# and Node.js: asyncevents aims to be a fully Pythonic library that provides just the
+features you need and nothing more (nor nothing less).
+
+## Cool! How do I use it?
+
+Like this
+
+```python3
+import time
+import asyncio
+from asyncevents import on_event, emit, wait
+
+
+@on_event("hello")
+async def hello(_, event: str):
+ print(f"Hello {event!r}!")
+
+
+@on_event("hi")
+async def hi(_, event: str):
+ print(f"Hi {event!r}! I'm going to sleep for 5 seconds")
+ await asyncio.sleep(5) # Simulates some work
+
+
+async def main():
+ print("Firing blocking event 'hello'")
+ await emit("hello") # This call blocks until hello() terminates
+ print("Handlers for event 'hello' have exited")
+ # Notice how, until here, the output is in order: this is on purpose!
+ # When using blocking mode, asyncevents even guarantees that handlers
+ # with different priorities will be executed in order
+ print("Firing blocking event 'hello'")
+ await emit("hi", block=False) # This one spawns hi() and returns immediately
+ print("Non-blocking event 'hello' fired")
+ await emit("event3") # Does nothing: No handlers registered for event3!
+ # We wait now for the the handler of the "hi" event to complete
+ t = time.time()
+ print("Waiting on event 'hi'")
+ await wait("hi") # Waits until all the handlers triggered by the "hi" event exit
+ print(f"Waited for {time.time() - t:.2f} seconds") # Should print roughly 5 seconds
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+
+## TODOs
+
+- Documentation
+- More tests
+- Trio/curio backend (maybe)
diff --git a/asyncevents/__init__.py b/asyncevents/__init__.py
new file mode 100644
index 0000000..e0c005b
--- /dev/null
+++ b/asyncevents/__init__.py
@@ -0,0 +1,97 @@
+# Copyright (C) 2021 nocturn9x
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import threading
+from asyncevents import events, errors
+from typing import Any, Callable, Coroutine, Optional
+from asyncevents.constants import ExceptionHandling, ExecutionMode, UnknownEventHandling
+
+# Thread-local namespace for storing the currently
+# used AsyncEventEmitter instance automatically
+local_storage: threading.local = threading.local()
+AsyncEventEmitter = events.AsyncEventEmitter
+
+
+def get_current_emitter():
+ """
+ Returns the currently active
+ emitter in the current thread
+ and creates a new one if one
+ doesn't exist
+ """
+
+ if not hasattr(local_storage, "emitter"):
+ local_storage.emitter = AsyncEventEmitter()
+ return local_storage.emitter
+
+
+def set_current_emitter(emitter: AsyncEventEmitter):
+ """
+ Sets the active emitter in the current thread.
+ Discards the existing one, if it exists
+
+ :param emitter: The new emitter to set
+ for the current thread
+ :type emitter: :class: AsyncEventEmitter
+ """
+
+ if not isinstance(emitter, AsyncEventEmitter) and not issubclass(type(emitter), AsyncEventEmitter):
+ raise TypeError(
+ "expected emitter to be an instance of AsyncEventEmitter or a subclass thereof,"
+ f" found {type(emitter).__name__!r} instead"
+ )
+ local_storage.emitter = emitter
+
+
+async def wait(event: Optional[str] = None):
+ """
+ Shorthand for get_current_emitter().wait()
+ """
+
+ await get_current_emitter().wait(event)
+
+
+async def emit(event: str, block: bool = True):
+ """
+ Shorthand for get_current_emitter().emit(event)
+ """
+
+ await get_current_emitter().emit(event, block)
+
+
+def on_event(event: str, priority: int = 0, emitter: AsyncEventEmitter = get_current_emitter(), oneshot: bool = False):
+ """
+ Decorator shorthand of emitter.register_event(event, f, priority)
+ """
+
+ def decorator(corofunc: Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]]):
+ emitter.register_event(event, corofunc, priority, oneshot)
+
+ async def wrapper(*args, **kwargs):
+ return await corofunc(*args, **kwargs)
+
+ return wrapper
+
+ return decorator
+
+
+__all__ = [
+ "events",
+ "errors",
+ "AsyncEventEmitter",
+ "ExceptionHandling",
+ "ExecutionMode",
+ "UnknownEventHandling",
+ "on_event",
+ "emit",
+ "wait",
+ "get_current_emitter",
+ "set_current_emitter"
+]
diff --git a/asyncevents/constants.py b/asyncevents/constants.py
new file mode 100644
index 0000000..b71572c
--- /dev/null
+++ b/asyncevents/constants.py
@@ -0,0 +1,67 @@
+# Copyright (C) 2021 nocturn9x
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from enum import Enum, auto, EnumMeta
+
+
+class ContainsEnumMeta(EnumMeta):
+ """
+ Simple metaclass that implements
+ the item in self operation
+ """
+
+ def __contains__(cls, item):
+ try:
+ cls(item)
+ except ValueError:
+ return False
+ else:
+ return True
+
+
+class ExceptionHandling(Enum, metaclass=ContainsEnumMeta):
+ """
+ Flags that control how exceptions
+ are handled in asyncevents. Note that,
+ following Python's conventions, only
+ subclasses of Exception are caught.
+ Any subclass of BaseException is ignored
+ as exceptions deriving directly from it,
+ aside from Exception itself, are not meant
+ to be caught
+ """
+
+ IGNORE: "ExceptionHandling" = auto() # The exception is caught and ignored
+ LOG: "ExceptionHandling" = auto() # The exception is caught and logged
+ PROPAGATE: "ExceptionHandling" = auto() # The exception is not caught at all
+
+
+class UnknownEventHandling(Enum, metaclass=ContainsEnumMeta):
+ """
+ Flags that control how the event emitter
+ handles events for which no task has been
+ registered. Note that this only applies to the
+ emit and the wait methods, and not to
+ register_handler/unregister_handler
+ """
+
+ IGNORE: "UnknownEventHandling" = auto() # Do nothing
+ LOG: "UnknownEventHandling" = auto() # Log it as a warning
+ ERROR: "UnknownEventHandling" = auto() # raise an UnknownEvent error
+
+
+class ExecutionMode(Enum, metaclass=ContainsEnumMeta):
+ """
+ Flags that control how the event emitter
+ spawns tasks
+ """
+
+ PAUSE: "ExecutionMode" = auto() # Spawn tasks via "await"
+ NOWAIT: "ExecutionMode" = auto() # Use asyncio.create_task
diff --git a/asyncevents/errors.py b/asyncevents/errors.py
new file mode 100644
index 0000000..f53d49d
--- /dev/null
+++ b/asyncevents/errors.py
@@ -0,0 +1,19 @@
+# Copyright (C) 2021 nocturn9x
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+class UnknownEvent(KeyError):
+ """
+ A KeyError subclass that is raised when
+ an unknown event is emitted (only when the
+ strategy of unknown event handling is set
+ to ERROR)
+ """
diff --git a/asyncevents/events.py b/asyncevents/events.py
new file mode 100644
index 0000000..7b161d0
--- /dev/null
+++ b/asyncevents/events.py
@@ -0,0 +1,393 @@
+# Copyright (C) 2021 nocturn9x
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import inspect
+import sys
+import time
+import asyncio
+from functools import partial
+from collections import defaultdict
+from asyncevents.errors import UnknownEvent
+from heapq import heappush, heapify, heappop
+from logging import Logger, getLogger, INFO, Formatter, StreamHandler
+from typing import Dict, List, Tuple, Coroutine, Callable, Any, Awaitable, Optional
+
+from asyncevents.constants import ExceptionHandling, UnknownEventHandling, ExecutionMode
+
+
+class AsyncEventEmitter:
+ """
+ A simple priority-based asynchronous event emitter. In contrast to a scheduler, which
+ continuously runs in the background orchestrating execution of _tasks, an emitter runs
+ only when an event is emitted and it only runs the _tasks that are meant to catch said
+ event, if any.
+
+ :param on_error: Tells the emitter what to do when an exception occurs inside an event
+ handler. This value can either be an entry from the asyncevents.ExceptionHandling
+ enum or a coroutine object. If the passed object is a coroutine, it is awaited
+ whenever an exception is caught with the AsyncEventEmitter instance, the exception
+ object and the event name as arguments (errors from the exception handler itself are
+ not caught). Defaults to ExceptionHandling.PROPAGATE, which lets exceptions fall trough
+ the execution chain (other enum values are LOG, which prints a log message on the
+ logging.ERROR level, and IGNORE which silences the exception entirely)
+ :type on_error: Union[ExceptionHandling, Callable[[AsyncEventEmitter, Exception, str], Coroutine[Any, Any, Any]]],
+ optional
+ :param on_unknown_event: Tells the emitter what to do when an unknown event is triggered. An
+ unknown event is an event for which no handler is registered (either because it has never
+ been registered or because all of its handlers have been removed). This value can either be
+ an entry from the asyncevents.UnknownEventHandling enum or a coroutine object. If the argument
+ is a coroutine, it is awaited with the AsyncEventEmitter instance and the event name as arguments.
+ Defaults to UnknownEventHandling.IGNORE, which does nothing (other enum values are LOG, which
+ prints a log message on the logging.WARNING level, and ERROR which raises an UnknownEvent exception)
+ Note: if the given callable is a coroutine, it is awaited, while it's called normally otherwise
+ and its return value is discarded
+ :type on_unknown_event: Union[UnknownEventHandling, Callable[[AsyncEventEmitter, str], Coroutine[Any, Any, Any]]], optional
+ :param mode: Tells the emitter how event handlers should be spawned. It should be an entry of the
+ the asyncevents.ExecutionMode enum. If it is set to ExecutionMode.PAUSE, the default, the event
+ emitter spawns _tasks by awaiting each matching handler: this causes it to pause on every handler.
+ If ExecutionMode.NOWAIT is used, the emitter uses asyncio.create_task to spawns all the handlers
+ at the same time (note though that using this mode kind of breaks the priority queueing: the handlers
+ are started according to their priorities, but once they are started they are handled by asyncio's
+ event loop which is non-deterministic, so expect some disorder). Using ExecutionMode.NOWAIT allows
+ to call the emitter's wait() method, which pauses until all currently running event handlers have
+ completed executing (when ExecutionMode.PAUSE is used, wait() is a no-op), but note that return
+ values from event handlers are not returned
+ :type mode: ExecutionMode
+ """
+
+ # Implementations for emit()
+
+ async def _check_event(self, event: str):
+ """
+ Performs checks about the given event
+ and raises/logs appropriately before
+ emitting/waiting on it
+
+ :param event: The event name
+ :type event: str
+ """
+
+ if self.handlers.get(event, None) is None:
+ if self.on_unknown_event == UnknownEventHandling.IGNORE:
+ return
+ elif self.on_unknown_event == UnknownEventHandling.ERROR:
+ raise UnknownEvent(f"unknown event {event!r}")
+ elif self.on_unknown_event == UnknownEventHandling.LOG:
+ self.logger.warning(f"Attempted to emit or wait on an unknown event {event!r}")
+ else:
+ # It's a coroutine! Call it
+ await self.on_unknown_event(self, event)
+
+ async def _catch_errors_in_awaitable(self, event: str, obj: Awaitable):
+ # 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
+ except Exception as e:
+ if (event, obj) in self._tasks:
+ obj: asyncio.Task # Silences PyCharm's warnings
+ self._tasks.remove((event, obj))
+ if inspect.iscoroutinefunction(self.on_error):
+ await self.on_error(self, e, event)
+ elif self.on_error == ExceptionHandling.PROPAGATE:
+ raise
+ elif self.on_error == ExceptionHandling.LOG:
+ self.logger.error(f"An exception occurred while handling {event!r}: {type(e).__name__} -> {e}")
+ # Note how the IGNORE case is excluded: we just do nothing after all
+
+ async def _emit_nowait(self, event: str):
+ # 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]] = []
+ while self.handlers[event]:
+ # We use heappop because we want the first
+ # by priority and the heap queue only has
+ # the guarantee we need for heap[0]
+ temp.append(heappop(self.handlers[event]))
+ task = asyncio.create_task(temp[-1][-2](self, event))
+ if temp[-1][-1]:
+ task.add_done_callback(
+ partial(
+ # The extra argument is the future asyncio passes us,
+ # which we don't care about
+ lambda s, ev, corofunc, _: s.unregister_handler(ev, corofunc),
+ self,
+ event,
+ temp[-1][-2],
+ )
+ )
+ self._tasks.append((event, asyncio.create_task(self._catch_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):
+ # This implementation of emit() returns only after
+ # all handlers have finished executing
+ await self._check_event(event)
+ temp: List[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]
+ while temp:
+ # We use heappop because we want the first
+ # by priority and the heap queue only has
+ # the guarantee we need for heap[0]
+ t = heappop(temp)
+ if t[-1]:
+ self.unregister_handler(event, t[-2])
+ await self._catch_errors_in_awaitable(event, t[-2](self, event))
+
+ def __init__(
+ self,
+ # These type hints come from https://stackoverflow.com/a/59177557/12159081
+ # and should correctly hint a coroutine function
+ 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,
+ mode: ExecutionMode = ExecutionMode.PAUSE,
+ ):
+ """
+ Public object constructor
+ """
+
+ if not inspect.iscoroutinefunction(on_error) and on_error not in ExceptionHandling:
+ if inspect.iscoroutine(on_unknown_event):
+ raise TypeError(
+ "on_unknown_event should be a coroutine *function*, not a coroutine! Pass the function"
+ " object without calling it!"
+ )
+ raise TypeError(
+ "expected on_error to be a coroutine function or an entry from the ExceptionHandling"
+ f" enum, found {type(on_error).__name__!r} instead"
+ )
+ if inspect.iscoroutinefunction(on_unknown_event) and on_unknown_event not in UnknownEventHandling:
+ if inspect.iscoroutine(on_unknown_event):
+ raise TypeError(
+ "on_unknown_event should be a coroutine *function*, not a coroutine! Pass the function"
+ " object without calling it!"
+ )
+ raise TypeError(
+ "expected on_unknown_event to be a coroutine function or an entry from the"
+ f" UnknownEventHandling enum, found {type(on_unknown_event).__name__!r} instead"
+ )
+ if mode not in ExecutionMode:
+ raise TypeError(
+ f"expected mode to be an entry from the ExecutionMode enum, found {type(mode).__name__!r}" " instead"
+ )
+ self.on_error = on_error
+ self.on_unknown_event = on_unknown_event
+ self.mode = mode
+ # Determines the implementation of emit()
+ # and wait() according to the provided
+ # settings and the current Python version
+ if self.mode == ExecutionMode.PAUSE:
+ self._emit_impl = self._emit_await
+ else:
+ self._emit_impl = self._emit_nowait
+ self.logger: Logger = getLogger("asyncevents")
+ self.logger.handlers = []
+ self.logger.setLevel(INFO)
+ handler: StreamHandler = StreamHandler(sys.stderr)
+ handler.setFormatter(Formatter(fmt="[%(levelname)s - %(asctime)s] %(message)s", datefmt="%d/%m/%Y %H:%M:%S %p"))
+ self.logger.addHandler(handler)
+ # Stores asyncio tasks so that wait() can call
+ # asyncio.gather() on them
+ self._tasks: List[Tuple[str, asyncio.Task]] = []
+ # Stores events and their priorities. Each
+ # entry in the dictionary is a (name, handlers)
+ # tuple where name is a string and handlers is
+ # list of tuples. Each tuple in the list contains
+ # a priority (defaults to 0), the insertion time of
+ # when the handler was registered (to act as a tie
+ # breaker for _tasks with identical priorities or
+ # when priorities aren't used at all) a coroutine
+ # function object and a boolean that signals if the
+ # 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]]
+ ] = defaultdict(list)
+
+ def exists(self, event: str) -> bool:
+ """
+ Returns if the given event has at least
+ one handler registered
+
+ :param event: The event name
+ :type event: str
+ :return: True if the event has at least one handler,
+ False otherwise
+ """
+
+ return len(self.handlers.get(event)) > 0
+
+ def register_event(
+ self,
+ event: str,
+ handler: Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]],
+ priority: int = 0,
+ oneshot: bool = False,
+ ):
+ """
+ Registers an event and its associated handler. If
+ the event is already registered, the given handler
+ is added to the already existing handler queue. Each
+ event will be called with the AsyncEventEmitter instance
+ that triggered the event as well as the event name itself
+
+ :param event: The event name
+ :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]]
+ :param priority: The handler's execution priority,
+ defaults to 0 (lower priority means earlier
+ execution!)
+ :type priority: int, optional
+ :param oneshot: If True, the event is unregistered after if fires the first time,
+ defaults to False
+ :type oneshot: bool, optional
+ """
+
+ heappush(self.handlers[event], (priority, time.monotonic(), handler, oneshot))
+
+ def unregister_event(self, event: str):
+ """
+ Unregisters all handlers for the given
+ event in one go. Does nothing if the
+ given event is not registered already and
+ raise_on_missing equals False (the default).
+ Note that this does not affect any
+ already started event handler for the
+ given event
+
+ :param event: The event name
+ :type event: str
+ :raises:
+ UnknownEvent: If self.on_unknown_error == UnknownEventHandling.ERROR
+ """
+
+ 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]:
+ """
+ Returns the tuple of (priority, date, corofunc, oneshot) representing the
+ given handler. Only the first matching entry is returned. If
+ raise_on_missing is False, None is returned if the given
+ event does not exist. False is returned if the given
+ handler is not registered for the given event
+
+ Note: This method is meant mostly for internal use
+
+ :param event: The event name
+ :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]]
+ :raises:
+ UnknownEvent: If self.on_unknown_error == UnknownEventHandling.ERROR
+ :returns: The tuple of (priority, date, coro) representing the
+ given handler
+ """
+
+ if not self.exists(event):
+ return None
+ for (priority, tie, corofunc, oneshot) in self.handlers[event]:
+ if corofunc == handler:
+ return priority, tie, corofunc, oneshot
+ return False
+
+ def unregister_handler(
+ self,
+ event: str,
+ handler: Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]],
+ remove_all: bool = False,
+ ):
+ """
+ Unregisters a given handler for the given event. If
+ remove_all is True (defaults to False), all occurrences
+ of the given handler are removed, otherwise only the first
+ one is unregistered. Does nothing if the given event is not
+ registered already and raise_on_missing equals False (the default).
+ This method does nothing if the given event exists, but the given
+ handler is not registered for it
+
+ :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]]
+ :param remove_all: If True, all occurrences of the
+ given handler are removed, otherwise only the first
+ one is unregistered
+ :type remove_all: bool, optional
+ :raises:
+ UnknownEvent: If self.on_unknown_error == UnknownEventHandling.ERROR
+ :return:
+ """
+
+ if remove_all:
+ for (priority, tie, coro, oneshot) in self.handlers[event]:
+ if handler == coro:
+ self.handlers[event].remove((priority, tie, coro, oneshot))
+ else:
+ if t := self._get(event, handler):
+ self.handlers[event].remove(t)
+ # We maintain the heap queue invariant
+ heapify(self.handlers[event])
+
+ async def wait(self, event: Optional[str] = None):
+ """
+ Waits until all the event handlers for the given
+ event have finished executing. When the given event
+ is None, the default, waits for all handlers of all
+ 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
+ """
+
+ if not event:
+ await asyncio.gather(*[t[1] for t in self._tasks])
+ self._tasks = []
+ else:
+ await self._check_event(event)
+ await asyncio.gather(*[t[1] for t in self._tasks if t[0] == event])
+
+ async def emit(self, event: str, block: bool = True):
+ """
+ Emits an event
+
+ :param event: The event to trigger. Note that,
+ depending on the configuration, unknown events
+ may raise errors or log to stderr
+ :type event: str
+ :param block: Temporarily overrides the emitter's global execution
+ mode. If block is True, the default, this call will pause until
+ execution of all event handlers has finished, otherwise it returns
+ as soon as they're scheduled
+ :type block: bool, optional
+ :raises:
+ UnknownEvent: If self.on_unknown_error == UnknownEventHandling.ERROR
+ and the given event is not registered
+ """
+
+ mode = self.mode
+ if block:
+ self.mode = ExecutionMode.PAUSE
+ self._emit_impl = self._emit_await
+ else:
+ self.mode = ExecutionMode.NOWAIT
+ self._emit_impl = self._emit_nowait
+ await self._emit_impl(event)
+ self.mode = mode
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..be10952
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1 @@
+setuptools~=59.2.0
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..b5f0cb7
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,32 @@
+# Copyright (C) 2021 nocturn9x
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import setuptools
+
+if __name__ == "__main__":
+ with open("README.md") as readme:
+ long_description = readme.read()
+ setuptools.setup(
+ name="asyncevents",
+ version="0.1",
+ author="Nocturn9x",
+ author_email="hackhab@gmail.com",
+ description="Asynchronous event handling in modern Python",
+ long_description=long_description,
+ long_description_content_type="text/markdown",
+ url="https://github.com/nocturn9x/asyncevents",
+ packages=setuptools.find_packages(),
+ classifiers=[
+ "Programming Language :: Python :: 3",
+ "Operating System :: OS Independent",
+ "License :: OSI Approved :: Apache License 2.0",
+ ],
+ python_requires=">=3.8",
+ )
diff --git a/tests/on_error.py b/tests/on_error.py
new file mode 100644
index 0000000..9e7d72e
--- /dev/null
+++ b/tests/on_error.py
@@ -0,0 +1,27 @@
+import asyncio
+from asyncevents import on_event, emit, get_current_emitter, ExceptionHandling
+
+
+@on_event("error")
+async def oh_no(_, event: str):
+ print("Goodbye!")
+ raise ValueError("D:")
+
+
+async def main():
+ try:
+ await emit("error") # The error propagates
+ except ValueError:
+ print("Bang!")
+ # Now let's try a different error handling strategy
+ get_current_emitter().on_error = ExceptionHandling.LOG # Logs the exception
+ await emit("error") # This won't raise. Yay!
+ print("We're safe!")
+ # And a different one again
+ get_current_emitter().on_error = ExceptionHandling.IGNORE # Silences the exception
+ await emit("error") # This won't raise nor log anything to the console. Yay x2!
+ print("We're safe again!")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/tests/simple_example.py b/tests/simple_example.py
new file mode 100644
index 0000000..98e954d
--- /dev/null
+++ b/tests/simple_example.py
@@ -0,0 +1,36 @@
+import time
+import asyncio
+from asyncevents import on_event, emit, wait
+
+
+@on_event("hello")
+async def hello(_, event: str):
+ print(f"Hello {event!r}!")
+
+
+@on_event("hi")
+async def hi(_, event: str):
+ print(f"Hi {event!r}! I'm going to sleep for 5 seconds")
+ await asyncio.sleep(5) # Simulates some work
+
+
+async def main():
+ print("Firing blocking event 'hello'")
+ await emit("hello") # This call blocks until hello() terminates
+ print("Handlers for event 'hello' have exited")
+ # Notice how, until here, the output is in order: this is on purpose!
+ # When using blocking mode, asyncevents even guarantees that handlers
+ # with different priorities will be executed in order
+ print("Firing blocking event 'hello'")
+ await emit("hi", block=False) # This one spawns hi() and returns immediately
+ print("Non-blocking event 'hello' fired")
+ await emit("event3") # Does nothing: No handlers registered for event3!
+ # We wait now for the the handler of the "hi" event to complete
+ t = time.time()
+ print("Waiting on event 'hi'")
+ await wait("hi") # Waits until all the handlers triggered by the "hi" event exit
+ print(f"Waited for {time.time() - t:.2f} seconds") # Should print roughly 5 seconds
+
+
+if __name__ == "__main__":
+ asyncio.run(main())