Major overhaul to the theme and now functioning.

This commit is contained in:
2026-01-05 16:07:30 -05:00
parent 5823ca3c39
commit a475dfeab6
5218 changed files with 1643729 additions and 13229 deletions
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
"""
pyee supplies a `EventEmitter` class that is similar to the
`EventEmitter` class from Node.js. In addition, it supplies the subclasses
`AsyncIOEventEmitter`, `TwistedEventEmitter` and `ExecutorEventEmitter`
for supporting async and threaded execution with asyncio, twisted, and
concurrent.futures Executors respectively, as supported by the environment.
# Example
```text
In [1]: from pyee.base import EventEmitter
In [2]: ee = EventEmitter()
In [3]: @ee.on('event')
...: def event_handler():
...: print('BANG BANG')
...:
In [4]: ee.emit('event')
BANG BANG
In [5]:
```
"""
from pyee.base import EventEmitter, Handler, PyeeError, PyeeException
__all__ = ["EventEmitter", "Handler", "PyeeError", "PyeeException"]
@@ -0,0 +1,179 @@
# -*- coding: utf-8 -*-
from asyncio import AbstractEventLoop, ensure_future, Future, iscoroutine, wait
from typing import Any, Callable, cast, Dict, Optional, Set, Tuple
from pyee.base import EventEmitter
Self = Any
__all__ = ["AsyncIOEventEmitter"]
class AsyncIOEventEmitter(EventEmitter):
"""An event emitter class which can run asyncio coroutines in addition to
synchronous blocking functions. For example:
```py
@ee.on('event')
async def async_handler(*args, **kwargs):
await returns_a_future()
```
On emit, the event emitter will automatically schedule the coroutine using
`asyncio.ensure_future` and the configured event loop (defaults to
`asyncio.get_event_loop()`).
Unlike the case with the EventEmitter, all exceptions raised by
event handlers are automatically emitted on the `error` event. This is
important for asyncio coroutines specifically but is also handled for
synchronous functions for consistency.
When `loop` is specified, the supplied event loop will be used when
scheduling work with `ensure_future`. Otherwise, the default asyncio
event loop is used.
For asyncio coroutine event handlers, calling emit is non-blocking.
In other words, you do not have to await any results from emit, and the
coroutine is scheduled in a fire-and-forget fashion.
"""
def __init__(self: Self, loop: Optional[AbstractEventLoop] = None) -> None:
super(AsyncIOEventEmitter, self).__init__()
self._loop: Optional[AbstractEventLoop] = loop
self._waiting: Set[Future] = set()
def emit(
self: Self,
event: str,
*args: Any,
**kwargs: Any,
) -> bool:
"""Emit `event`, passing `*args` and `**kwargs` to each attached
function or coroutine. Returns `True` if any functions are attached to
`event`; otherwise returns `False`.
Example:
```py
ee.emit('data', '00101001')
```
Assuming `data` is an attached function, this will call
`data('00101001')'`.
When executing coroutine handlers, their respective futures will be
stored in a "waiting" state. These futures may be waited on or
canceled with `wait_for_complete` and `cancel`, respectively; and
their status may be checked via the `complete` property.
"""
return super().emit(event, *args, **kwargs)
def _emit_run(
self: Self,
f: Callable,
args: Tuple[Any, ...],
kwargs: Dict[str, Any],
) -> None:
try:
coro: Any = f(*args, **kwargs)
except Exception as exc:
self.emit("error", exc)
else:
if iscoroutine(coro):
if self._loop:
# ensure_future is *extremely* cranky about the types here,
# but this is relatively well-tested and I think the types
# are more strict than they should be
fut: Any = ensure_future(cast(Any, coro), loop=self._loop)
else:
fut = ensure_future(cast(Any, coro))
elif isinstance(coro, Future):
fut = cast(Any, coro)
else:
return
def callback(f: Future) -> None:
self._waiting.discard(f)
if f.cancelled():
return
exc: Optional[BaseException] = f.exception()
if exc:
self.emit("error", exc)
fut.add_done_callback(callback)
self._waiting.add(fut)
async def wait_for_complete(self: Self) -> None:
"""Waits for all pending tasks to complete. For example:
```py
@ee.on('event')
async def async_handler(*args, **kwargs):
await returns_a_future()
# Triggers execution of async_handler
ee.emit('data', '00101001')
await ee.wait_for_complete()
# async_handler has completed execution
```
This is useful if you're attempting a graceful shutdown of your
application and want to ensure all coroutines have completed execution
beforehand.
"""
if self._waiting:
await wait(self._waiting)
def cancel(self: Self) -> None:
"""Cancel all pending tasks. For example:
```py
@ee.on('event')
async def async_handler(*args, **kwargs):
await returns_a_future()
# Triggers execution of async_handler
ee.emit('data', '00101001')
ee.cancel()
# async_handler execution has been canceled
```
This is useful if you're attempting to shut down your application and
attempts at a graceful shutdown via `wait_for_complete` have failed.
"""
for fut in self._waiting:
if not fut.done() and not fut.cancelled():
fut.cancel()
self._waiting.clear()
@property
def complete(self: Self) -> bool:
"""When true, there are no pending tasks, and execution is complete.
For example:
```py
@ee.on('event')
async def async_handler(*args, **kwargs):
await returns_a_future()
# Triggers execution of async_handler
ee.emit('data', '00101001')
# async_handler is still running, so this prints False
print(ee.complete)
await ee.wait_for_complete()
# async_handler has completed execution, so this prints True
print(ee.complete)
```
"""
return not self._waiting
@@ -0,0 +1,283 @@
# -*- coding: utf-8 -*-
from collections import OrderedDict
from threading import Lock
from typing import (
Any,
Callable,
Dict,
List,
Mapping,
Optional,
overload,
Set,
Tuple,
TypeVar,
Union,
)
Self = Any
class PyeeException(Exception):
"""An exception internal to pyee. Deprecated in favor of PyeeError."""
class PyeeError(PyeeException):
"""An error internal to pyee."""
Handler = TypeVar("Handler", bound=Callable)
class EventEmitter:
"""The base event emitter class. All other event emitters inherit from
this class.
Most events are registered with an emitter via the `on` and `once`
methods, and fired with the `emit` method. However, pyee event emitters
have two *special* events:
- `new_listener`: Fires whenever a new listener is created. Listeners for
this event do not fire upon their own creation.
- `error`: When emitted raises an Exception by default, behavior can be
overridden by attaching callback to the event.
For example:
```py
@ee.on('error')
def on_error(message):
logging.err(message)
ee.emit('error', Exception('something blew up'))
```
All callbacks are handled in a synchronous, blocking manner. As in node.js,
raised exceptions are not automatically handled for you---you must catch
your own exceptions, and treat them accordingly.
"""
def __init__(self: Self) -> None:
self._events: Dict[
str,
"OrderedDict[Callable, Callable]",
] = dict()
self._lock: Lock = Lock()
def __getstate__(self: Self) -> Mapping[str, Any]:
state = self.__dict__.copy()
del state["_lock"]
return state
def __setstate__(self: Self, state: Mapping[str, Any]) -> None:
self.__dict__.update(state)
self._lock = Lock()
@overload
def on(self: Self, event: str) -> Callable[[Handler], Handler]: ...
@overload
def on(self: Self, event: str, f: Handler) -> Handler: ...
def on(
self: Self, event: str, f: Optional[Handler] = None
) -> Union[Handler, Callable[[Handler], Handler]]:
"""Registers the function `f` to the event name `event`, if provided.
If `f` isn't provided, this method calls `EventEmitter#listens_to`, and
otherwise calls `EventEmitter#add_listener`. In other words, you may either
use it as a decorator:
```py
@ee.on('data')
def data_handler(data):
print(data)
```
Or directly:
```py
ee.on('data', data_handler)
```
In both the decorated and undecorated forms, the event handler is
returned. The upshot of this is that you can call decorated handlers
directly, as well as use them in remove_listener calls.
Note that this method's return type is a union type. If you are using
mypy or pyright, you will probably want to use either
`EventEmitter#listens_to` or `EventEmitter#add_listener`.
"""
if f is None:
return self.listens_to(event)
else:
return self.add_listener(event, f)
def listens_to(self: Self, event: str) -> Callable[[Handler], Handler]:
"""Returns a decorator which will register the decorated function to
the event name `event`:
```py
@ee.listens_to("event")
def data_handler(data):
print(data)
```
By only supporting the decorator use case, this method has improved
type safety over `EventEmitter#on`.
"""
def on(f: Handler) -> Handler:
self._add_event_handler(event, f, f)
return f
return on
def add_listener(self: Self, event: str, f: Handler) -> Handler:
"""Register the function `f` to the event name `event`:
```
def data_handler(data):
print(data)
h = ee.add_listener("event", data_handler)
```
By not supporting the decorator use case, this method has improved
type safety over `EventEmitter#on`.
"""
self._add_event_handler(event, f, f)
return f
def _add_event_handler(self: Self, event: str, k: Callable, v: Callable):
# Fire 'new_listener' *before* adding the new listener!
self.emit("new_listener", event, k)
# Add the necessary function
# Note that k and v are the same for `on` handlers, but
# different for `once` handlers, where v is a wrapped version
# of k which removes itself before calling k
with self._lock:
if event not in self._events:
self._events[event] = OrderedDict()
self._events[event][k] = v
def _emit_run(
self: Self,
f: Callable,
args: Tuple[Any, ...],
kwargs: Dict[str, Any],
) -> None:
f(*args, **kwargs)
def event_names(self: Self) -> Set[str]:
"""Get a set of events that this emitter is listening to."""
return set(self._events.keys())
def _emit_handle_potential_error(self: Self, event: str, error: Any) -> None:
if event == "error":
if isinstance(error, Exception):
raise error
else:
raise PyeeError(f"Uncaught, unspecified 'error' event: {error}")
def _call_handlers(
self: Self,
event: str,
args: Tuple[Any, ...],
kwargs: Dict[str, Any],
) -> bool:
handled = False
with self._lock:
funcs = list(self._events.get(event, OrderedDict()).values())
for f in funcs:
self._emit_run(f, args, kwargs)
handled = True
return handled
def emit(
self: Self,
event: str,
*args: Any,
**kwargs: Any,
) -> bool:
"""Emit `event`, passing `*args` and `**kwargs` to each attached
function. Returns `True` if any functions are attached to `event`;
otherwise returns `False`.
Example:
```py
ee.emit('data', '00101001')
```
Assuming `data` is an attached function, this will call
`data('00101001')'`.
"""
handled = self._call_handlers(event, args, kwargs)
if not handled:
self._emit_handle_potential_error(event, args[0] if args else None)
return handled
def once(
self: Self,
event: str,
f: Optional[Callable] = None,
) -> Callable:
"""The same as `ee.on`, except that the listener is automatically
removed after being called.
"""
def _wrapper(f: Callable) -> Callable:
def g(
*args: Any,
**kwargs: Any,
) -> Any:
with self._lock:
# Check that the event wasn't removed already right
# before the lock
if event in self._events and f in self._events[event]:
self._remove_listener(event, f)
else:
return None
# f may return a coroutine, so we need to return that
# result here so that emit can schedule it
return f(*args, **kwargs)
self._add_event_handler(event, f, g)
return f
if f is None:
return _wrapper
else:
return _wrapper(f)
def _remove_listener(self: Self, event: str, f: Callable) -> None:
"""Naked unprotected removal."""
self._events[event].pop(f)
if not len(self._events[event]):
del self._events[event]
def remove_listener(self: Self, event: str, f: Callable) -> None:
"""Removes the function `f` from `event`."""
with self._lock:
self._remove_listener(event, f)
def remove_all_listeners(self: Self, event: Optional[str] = None) -> None:
"""Remove all listeners attached to `event`.
If `event` is `None`, remove all listeners on all events.
"""
with self._lock:
if event is not None:
self._events[event] = OrderedDict()
else:
self._events = dict()
def listeners(self: Self, event: str) -> List[Callable]:
"""Returns a list of all listeners registered to the `event`."""
return list(self._events.get(event, OrderedDict()).keys())
@@ -0,0 +1,116 @@
from dataclasses import dataclass
from functools import wraps
from typing import Any, Callable, Iterator, List, Type, TypeVar
from pyee import EventEmitter
@dataclass
class Handler:
event: str
method: Callable
class Handlers:
def __init__(self) -> None:
self._handlers: List[Handler] = []
def append(self, handler) -> None:
self._handlers.append(handler)
def __iter__(self) -> Iterator[Handler]:
return iter(self._handlers)
def reset(self):
self._handlers = []
_handlers = Handlers()
def on(event: str) -> Callable[[Callable], Callable]:
"""
Register an event handler on an evented class. See the `evented` class
decorator for a full example.
"""
def decorator(method: Callable) -> Callable:
_handlers.append(Handler(event=event, method=method))
return method
return decorator
def _bind(self: Any, method: Any) -> Any:
@wraps(method)
def bound(*args, **kwargs) -> Any:
return method(self, *args, **kwargs)
return bound
Cls = TypeVar("Cls", bound=Type)
def evented(cls: Cls) -> Cls:
"""
Configure an evented class.
Evented classes are classes which use an EventEmitter to call instance
methods during runtime. To achieve this without this helper, you would
instantiate an `EventEmitter` in the `__init__` method and then call
`event_emitter.on` for every method on `self`.
This decorator and the `on` function help make things look a little nicer
by defining the event handler on the method in the class and then adding
the `__init__` hook in a wrapper:
```py
from pyee.cls import evented, on
@evented
class Evented:
@on("event")
def event_handler(self, *args, **kwargs):
print(self, args, kwargs)
evented_obj = Evented()
evented_obj.event_emitter.emit(
"event", "hello world", numbers=[1, 2, 3]
)
```
The `__init__` wrapper will create a `self.event_emitter: EventEmitter`
automatically but you can also define your own event_emitter inside your
class's unwrapped `__init__` method. For example, to use this
decorator with a `TwistedEventEmitter`::
```py
@evented
class Evented:
def __init__(self):
self.event_emitter = TwistedEventEmitter()
@on("event")
async def event_handler(self, *args, **kwargs):
await self.some_async_action(*args, **kwargs)
```
"""
handlers: List[Handler] = list(_handlers)
_handlers.reset()
og_init: Callable = cls.__init__
@wraps(cls.__init__)
def init(self: Any, *args: Any, **kwargs: Any) -> None:
og_init(self, *args, **kwargs)
if not hasattr(self, "event_emitter"):
self.event_emitter = EventEmitter()
for h in handlers:
self.event_emitter.on(h.event, _bind(self, h.method))
cls.__init__ = init
return cls
@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
from concurrent.futures import Executor, Future, ThreadPoolExecutor
from types import TracebackType
from typing import Any, Callable, Dict, Optional, Tuple, Type
from pyee.base import EventEmitter
Self = Any
__all__ = ["ExecutorEventEmitter"]
class ExecutorEventEmitter(EventEmitter):
"""An event emitter class which runs handlers in a `concurrent.futures`
executor.
By default, this class creates a default `ThreadPoolExecutor`, but
a custom executor may also be passed in explicitly to, for instance,
use a `ProcessPoolExecutor` instead.
This class runs all emitted events on the configured executor. Errors
captured by the resulting Future are automatically emitted on the
`error` event. This is unlike the EventEmitter, which have no error
handling.
The underlying executor may be shut down by calling the `shutdown`
method. Alternately you can treat the event emitter as a context manager:
```py
with ExecutorEventEmitter() as ee:
# Underlying executor open
@ee.on('data')
def handler(data):
print(data)
ee.emit('event')
# Underlying executor closed
```
Since the function call is scheduled on an executor, emit is always
non-blocking.
No effort is made to ensure thread safety, beyond using an executor.
"""
def __init__(self: Self, executor: Optional[Executor] = None) -> None:
super(ExecutorEventEmitter, self).__init__()
if executor:
self._executor: Executor = executor
else:
self._executor = ThreadPoolExecutor()
def _emit_run(
self: Self,
f: Callable,
args: Tuple[Any, ...],
kwargs: Dict[str, Any],
) -> None:
future: Future = self._executor.submit(f, *args, **kwargs)
@future.add_done_callback
def _callback(f: Future) -> None:
exc: Optional[BaseException] = f.exception()
if isinstance(exc, Exception):
self.emit("error", exc)
elif exc is not None:
raise exc
def shutdown(self: Self, wait: bool = True) -> None:
"""Call `shutdown` on the internal executor."""
self._executor.shutdown(wait=wait)
def __enter__(self: Self) -> "ExecutorEventEmitter":
return self
def __exit__(
self: Self, type: Type[Exception], value: Exception, traceback: TracebackType
) -> Optional[bool]:
self.shutdown()
return None
@@ -0,0 +1,143 @@
# -*- coding: utf-8 -*-
from contextlib import AbstractAsyncContextManager, asynccontextmanager
from types import TracebackType
from typing import (
Any,
AsyncGenerator,
Awaitable,
Callable,
cast,
Dict,
Optional,
Tuple,
Type,
)
import trio
from pyee.base import EventEmitter, PyeeError
Self = Any
__all__ = ["TrioEventEmitter"]
Nursery = trio.Nursery
class TrioEventEmitter(EventEmitter):
"""An event emitter class which can run trio tasks in a trio nursery.
By default, this class will lazily create both a nursery manager (the
object returned from `trio.open_nursery()` and a nursery (the object
yielded by using the nursery manager as an async context manager). It is
also possible to supply an existing nursery manager via the `manager`
argument, or an existing nursery via the `nursery` argument.
Instances of TrioEventEmitter are themselves async context managers, so
that they may manage the lifecycle of the underlying trio nursery. For
example, typical usage of this library may look something like this::
```py
async with TrioEventEmitter() as ee:
# Underlying nursery is instantiated and ready to go
@ee.on('data')
async def handler(data):
print(data)
ee.emit('event')
# Underlying nursery and manager have been cleaned up
```
Unlike the case with the EventEmitter, all exceptions raised by event
handlers are automatically emitted on the `error` event. This is
important for trio coroutines specifically but is also handled for
synchronous functions for consistency.
For trio coroutine event handlers, calling emit is non-blocking. In other
words, you should not attempt to await emit; the coroutine is scheduled
in a fire-and-forget fashion.
"""
def __init__(
self: Self,
nursery: Optional[Nursery] = None,
manager: Optional["AbstractAsyncContextManager[trio.Nursery]"] = None,
):
super(TrioEventEmitter, self).__init__()
self._nursery: Optional[Nursery] = None
self._manager: Optional["AbstractAsyncContextManager[trio.Nursery]"] = None
if nursery:
if manager:
raise PyeeError(
"You may either pass a nursery or a nursery manager " "but not both"
)
self._nursery = nursery
elif manager:
self._manager = manager
else:
self._manager = trio.open_nursery()
def _async_runner(
self: Self,
f: Callable,
args: Tuple[Any, ...],
kwargs: Dict[str, Any],
) -> Callable[[], Awaitable[None]]:
async def runner() -> None:
try:
await f(*args, **kwargs)
except Exception as exc:
self.emit("error", exc)
return runner
def _emit_run(
self: Self,
f: Callable,
args: Tuple[Any, ...],
kwargs: Dict[str, Any],
) -> None:
if not self._nursery:
raise PyeeError("Uninitialized trio nursery")
self._nursery.start_soon(self._async_runner(f, args, kwargs))
@asynccontextmanager
async def context(
self: Self,
) -> AsyncGenerator["TrioEventEmitter", None]:
"""Returns an async contextmanager which manages the underlying
nursery to the EventEmitter. The `TrioEventEmitter`'s
async context management methods are implemented using this
function, but it may also be used directly for clarity.
"""
if self._nursery is not None:
yield self
elif self._manager is not None:
async with self._manager as nursery:
self._nursery = nursery
yield self
else:
raise PyeeError("Uninitialized nursery or nursery manager")
async def __aenter__(self: Self) -> "TrioEventEmitter":
self._context: Optional[AbstractAsyncContextManager["TrioEventEmitter"]] = (
self.context()
)
return await cast(Any, self._context).__aenter__()
async def __aexit__(
self: Self,
type: Optional[Type[BaseException]],
value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> Optional[bool]:
if self._context is None:
raise PyeeError("Attempting to exit uninitialized context")
rv = await self._context.__aexit__(type, value, traceback)
self._context = None
self._nursery = None
self._manager = None
return rv
@@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
from asyncio import iscoroutine
from typing import Any, Callable, cast, Dict, Optional, Tuple
from twisted.internet.defer import Deferred, ensureDeferred
from twisted.python.failure import Failure
from pyee.base import EventEmitter, PyeeError
Self = Any
__all__ = ["TwistedEventEmitter"]
class TwistedEventEmitter(EventEmitter):
"""An event emitter class which can run twisted coroutines and handle
returned Deferreds, in addition to synchronous blocking functions. For
example:
```py
@ee.on('event')
@inlineCallbacks
def async_handler(*args, **kwargs):
yield returns_a_deferred()
```
or:
```py
@ee.on('event')
async def async_handler(*args, **kwargs):
await returns_a_deferred()
```
When async handlers fail, Failures are first emitted on the `failure`
event. If there are no `failure` handlers, the Failure's associated
exception is then emitted on the `error` event. If there are no `error`
handlers, the exception is raised. For consistency, when handlers raise
errors synchronously, they're captured, wrapped in a Failure and treated
as an async failure. This is unlike the behavior of EventEmitter,
which have no special error handling.
For twisted coroutine event handlers, calling emit is non-blocking.
In other words, you do not have to await any results from emit, and the
coroutine is scheduled in a fire-and-forget fashion.
Similar behavior occurs for "sync" functions which return Deferreds.
"""
def __init__(self: Self) -> None:
super(TwistedEventEmitter, self).__init__()
def _emit_run(
self: Self,
f: Callable,
args: Tuple[Any, ...],
kwargs: Dict[str, Any],
) -> None:
d: Optional[Deferred[Any]] = None
try:
result = f(*args, **kwargs)
except Exception:
self.emit("failure", Failure())
else:
if iscoroutine(result):
d = ensureDeferred(result)
elif isinstance(result, Deferred):
d = result
elif not d:
return
def errback(failure: Failure) -> None:
if failure:
self.emit("failure", failure)
d.addErrback(errback)
def _emit_handle_potential_error(self: Self, event: str, error: Any) -> None:
if event == "failure":
if isinstance(error, Failure):
try:
error.raiseException()
except Exception as exc:
self.emit("error", exc)
elif isinstance(error, Exception):
self.emit("error", error)
else:
self.emit("error", PyeeError(f"Unexpected failure object: {error}"))
else:
cast(Any, super(TwistedEventEmitter, self))._emit_handle_potential_error(
event, error
)
@@ -0,0 +1,178 @@
# -*- coding: utf-8 -*-
from functools import wraps
from typing import Any, Callable, cast, Dict, Optional, Tuple, Type, TypeVar, Union
import warnings
from typing_extensions import Literal
from pyee.base import EventEmitter
UpliftingEventEmitter = TypeVar("UpliftingEventEmitter", bound=EventEmitter)
EMIT_WRAPPERS: Dict[EventEmitter, Callable[[], None]] = dict()
def unwrap(event_emitter: EventEmitter) -> None:
"""Unwrap an uplifted EventEmitter, returning it to its prior state."""
if event_emitter in EMIT_WRAPPERS:
EMIT_WRAPPERS[event_emitter]()
def _wrap(
left: EventEmitter,
right: EventEmitter,
error_handler: Any,
proxy_new_listener: bool,
) -> None:
left_emit = left.emit
left_unwrap: Optional[Callable[[], None]] = EMIT_WRAPPERS.get(left)
@wraps(left_emit)
def wrapped_emit(event: str, *args: Any, **kwargs: Any) -> bool:
left_handled: bool = left._call_handlers(event, args, kwargs)
# Do it for the right side
if proxy_new_listener or event != "new_listener":
right_handled = right._call_handlers(event, args, kwargs)
else:
right_handled = False
handled = left_handled or right_handled
# Use the error handling on `error_handler` (should either be
# `left` or `right`)
if not handled:
error_handler._emit_handle_potential_error(event, args[0] if args else None)
return handled
def _unwrap() -> None:
warnings.warn(
DeprecationWarning(
"Patched ee.unwrap() is deprecated and will be removed in a "
"future release. Use pyee.uplift.unwrap instead."
)
)
unwrap(left)
def unwrap_hook() -> None:
cast(Any, left).emit = left_emit
if left_unwrap:
EMIT_WRAPPERS[left] = left_unwrap
else:
del EMIT_WRAPPERS[left]
del left.unwrap # type: ignore
cast(Any, left).emit = left_emit
unwrap(right)
cast(Any, left).emit = wrapped_emit
EMIT_WRAPPERS[left] = unwrap_hook
left.unwrap = _unwrap # type: ignore
_PROXY_NEW_LISTENER_SETTINGS: Dict[str, Tuple[bool, bool]] = dict(
forward=(False, True),
backward=(True, False),
both=(True, True),
neither=(False, False),
)
ErrorStrategy = Union[Literal["new"], Literal["underlying"], Literal["neither"]]
ProxyStrategy = Union[
Literal["forward"], Literal["backward"], Literal["both"], Literal["neither"]
]
def uplift(
cls: Type[UpliftingEventEmitter],
underlying: EventEmitter,
error_handling: ErrorStrategy = "new",
proxy_new_listener: ProxyStrategy = "forward",
*args: Any,
**kwargs: Any,
) -> UpliftingEventEmitter:
"""A helper to create instances of an event emitter `cls` that inherits
event behavior from an `underlying` event emitter instance.
This is mostly helpful if you have a simple underlying event emitter
that you don't have direct control over, but you want to use that
event emitter in a new context - for example, you may want to `uplift` a
`EventEmitter` supplied by a third party library into an
`AsyncIOEventEmitter` so that you may register async event handlers
in your `asyncio` app but still be able to receive events from the
underlying event emitter and call the underlying event emitter's existing
handlers.
When called, `uplift` instantiates a new instance of `cls`, passing
along any unrecognized arguments, and overwrites the `emit` method on
the `underlying` event emitter to also emit events on the new event
emitter and vice versa. In both cases, they return whether the `emit`
method was handled by either emitter. Execution order prefers the event
emitter on which `emit` was called.
The `unwrap` function may be called on either instance; this will
unwrap both `emit` methods.
The `error_handling` flag can be configured to control what happens to
unhandled errors:
- 'new': Error handling for the new event emitter is always used and the
underlying library's non-event-based error handling is inert.
- 'underlying': Error handling on the underlying event emitter is always
used and the new event emitter can not implement non-event-based error
handling.
- 'neither': Error handling for the new event emitter is used if the
handler was registered on the new event emitter, and vice versa.
Tuning this option can be useful depending on how the underlying event
emitter does error handling. The default is 'new'.
The `proxy_new_listener` option can be configured to control how
`new_listener` events are treated:
- 'forward': `new_listener` events are propagated from the underlying
event emitter to the new event emitter but not vice versa.
- 'both': `new_listener` events are propagated as with other events.
- 'neither': `new_listener` events are only fired on their respective
event emitters.
- 'backward': `new_listener` events are propagated from the new event
emitter to the underlying event emitter, but not vice versa.
Tuning this option can be useful depending on how the `new_listener`
event is used by the underlying event emitter, if at all. The default is
'forward', since `underlying` may not know how to handle certain
handlers, such as asyncio coroutines.
Each event emitter tracks its own internal table of handlers.
`remove_listener`, `remove_all_listeners` and `listeners` all
work independently. This means you will have to remember which event
emitter an event handler was added to!
Note that both the new event emitter returned by `cls` and the
underlying event emitter should inherit from `EventEmitter`, or at
least implement the interface for the undocumented `_call_handlers` and
`_emit_handle_potential_error` methods.
"""
(
new_proxy_new_listener,
underlying_proxy_new_listener,
) = _PROXY_NEW_LISTENER_SETTINGS[proxy_new_listener]
new: UpliftingEventEmitter = cls(*args, **kwargs)
uplift_error_handlers: Dict[str, Tuple[EventEmitter, EventEmitter]] = dict(
new=(new, new), underlying=(underlying, underlying), neither=(new, underlying)
)
new_error_handler, underlying_error_handler = uplift_error_handlers[error_handling]
_wrap(new, underlying, new_error_handler, new_proxy_new_listener)
_wrap(underlying, new, underlying_error_handler, underlying_proxy_new_listener)
return new