Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ No test file calls `pytest.importorskip`. `tests/conftest.py` ignores `test_asyn

## Conventions

The public emitter classes, plus `uplift`, `unwrap`, `evented`, and `on`, use a triple-quoted docstring. The text is prose, and names sit in backticks. Examples, when present, are Markdown fences. The docstrings have no `Args`, `Returns`, or `Raises` sections. `pyee/__init__.py` holds the package docstring, including an IPython session. The other modules have no module docstring.
The public emitter classes, plus `uplift`, `unwrap`, `evented`, and `on`, use a triple-quoted docstring. The text is prose, and names sit in backticks. Examples, when present, are Markdown fences. After the prose, the docstrings use Google-style `Args`, `Returns`, and `Raises` sections where they apply. mkdocstrings renders these sections as tables. The internal `Handler` and `Handlers` classes have no such sections. `pyee/__init__.py` holds the package docstring, including an IPython session. The other modules have no module docstring.

`pyee/base.py`, `pyee/asyncio.py`, `pyee/twisted.py`, `pyee/executor.py`, and `pyee/trio.py` set `Self = Any` and annotate `self` as `Self`. Keep that. Pyright and mypy both have to pass.

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## xxx/yy/zz Version x.y.z

- 🤖 `AGENTS.md` file for AI coding assistant guidance
- 🤖 Add `Args`, `Returns` and `Raises` sections to public docstrings (#221)
- Type check with both `pyright` and `mypy` by default
- Remove `tox` test matrix, leaning on Github Actions

Expand Down
24 changes: 24 additions & 0 deletions pyee/asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ async def async_handler(*args, **kwargs):
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.

Args:
loop: Event loop passed to `asyncio.ensure_future` when scheduling
coroutine handlers. `None` uses `ensure_future` without a loop.
"""

def __init__(self: Self, loop: Optional[AbstractEventLoop] = None) -> None:
Expand Down Expand Up @@ -66,6 +70,23 @@ def emit(
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.

Args:
event: Event name to emit.
args: Positional arguments passed to each handler.
kwargs: Keyword arguments passed to each handler.

Returns:
`True` if any handlers are attached to `event`, otherwise
`False`.

Raises:
Exception: When `event` is `"error"` and no handlers are
attached, the first positional argument is raised if it is
an `Exception`.
PyeeError: When `event` is `"error"`, no handlers are attached,
and the first positional argument is missing or is not an
`Exception`.
"""
return super().emit(event, *args, **kwargs)

Expand Down Expand Up @@ -177,5 +198,8 @@ async def async_handler(*args, **kwargs):
# async_handler has completed execution, so this prints True
print(ee.complete)
```

Returns:
`True` when no handler futures are pending, otherwise `False`.
"""
return not self._waiting
75 changes: 73 additions & 2 deletions pyee/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ def data_handler(data):
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`.

Args:
event: Event name to listen for.
f: Handler to register. Omit `f` to use this method as a
decorator.

Returns:
The handler `f` when it is given. When `f` is omitted, a
decorator that registers the decorated function and returns
it.
"""
if f is None:
return self.listens_to(event)
Expand All @@ -126,6 +136,13 @@ def data_handler(data):

By only supporting the decorator use case, this method has improved
type safety over `EventEmitter#on`.

Args:
event: Event name the decorated function listens for.

Returns:
A decorator that registers the decorated function for `event`
and returns that function.
"""

def on(f: Handler) -> Handler:
Expand All @@ -146,6 +163,13 @@ def data_handler(data):

By not supporting the decorator use case, this method has improved
type safety over `EventEmitter#on`.

Args:
event: Event name to listen for.
f: Handler to register.

Returns:
The handler `f`.
"""
self._add_event_handler(event, f, f)
return f
Expand All @@ -172,7 +196,11 @@ def _emit_run(
f(*args, **kwargs)

def event_names(self: Self) -> Set[str]:
"""Get a set of events that this emitter is listening to."""
"""Get a set of events that this emitter is listening to.

Returns:
A set of event names that currently have listeners.
"""
return set(self._events.keys())

def _emit_handle_potential_error(self: Self, event: str, error: Any) -> None:
Expand Down Expand Up @@ -216,6 +244,23 @@ def emit(

Assuming `data` is an attached function, this will call
`data('00101001')'`.

Args:
event: Event name to emit.
args: Positional arguments passed to each handler.
kwargs: Keyword arguments passed to each handler.

Returns:
`True` if any handlers are attached to `event`, otherwise
`False`.

Raises:
Exception: When `event` is `"error"` and no handlers are
attached, the first positional argument is raised if it is
an `Exception`.
PyeeError: When `event` is `"error"`, no handlers are attached,
and the first positional argument is missing or is not an
`Exception`.
"""
handled = self._call_handlers(event, args, kwargs)

Expand All @@ -231,6 +276,16 @@ def once(
) -> Callable:
"""The same as `ee.on`, except that the listener is automatically
removed after being called.

Args:
event: Event name to listen for once.
f: Handler to register. Omit `f` to use this method as a
decorator.

Returns:
The handler `f` when it is given. When `f` is omitted, a
decorator that registers the decorated function and returns
it.
"""

def _wrapper(f: Callable) -> Callable:
Expand Down Expand Up @@ -267,13 +322,22 @@ def _remove_listener(self: Self, event: str, f: Callable) -> None:
def remove_listener(self: Self, event: str, f: Callable) -> None:
"""Removes the function `f` from `event`.
If `f` is not registered to `event`, this has no effect.

Args:
event: Event name to remove `f` from.
f: Handler to remove. Has no effect when `f` is not registered
for `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.

Args:
event: Event whose listeners are removed. `None` removes
listeners for every event.
"""
with self._lock:
if event is not None:
Expand All @@ -283,5 +347,12 @@ def remove_all_listeners(self: Self, event: Optional[str] = None) -> None:
self._events = dict()

def listeners(self: Self, event: str) -> List[Callable]:
"""Returns a list of all listeners registered to the `event`."""
"""Returns a list of all listeners registered to the `event`.

Args:
event: Event name.

Returns:
Handlers registered for `event`, in the order they were added.
"""
return list(self._events.get(event, OrderedDict()).keys())
14 changes: 14 additions & 0 deletions pyee/cls.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ def on(event: str) -> Callable[[Callable], Callable]:
"""
Register an event handler on an evented class. See the `evented` class
decorator for a full example.

Args:
event: Event name the decorated method handles.

Returns:
A decorator that records the method for `evented` and returns the
method unchanged.
"""

def decorator(method: Callable) -> Callable:
Expand Down Expand Up @@ -96,6 +103,13 @@ def __init__(self):
async def event_handler(self, *args, **kwargs):
await self.some_async_action(*args, **kwargs)
```

Args:
cls: Class to configure. `__init__` is wrapped so each instance
registers the methods marked with `on`.

Returns:
The same class, after `__init__` is wrapped.
"""
handlers: List[Handler] = list(_handlers)
_handlers.reset()
Expand Down
11 changes: 10 additions & 1 deletion pyee/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ def handler(data):
non-blocking.

No effort is made to ensure thread safety, beyond using an executor.

Args:
executor: Executor that runs handlers. `None` creates a
`ThreadPoolExecutor`.
"""

def __init__(self: Self, executor: Optional[Executor] = None) -> None:
Expand All @@ -70,7 +74,12 @@ def _callback(f: Future) -> None:
raise exc

def shutdown(self: Self, wait: bool = True) -> None:
"""Call `shutdown` on the internal executor."""
"""Call `shutdown` on the internal executor.

Args:
wait: Passed through to `Executor.shutdown`. `True` waits until
queued work finishes. Defaults to `True`.
"""

self._executor.shutdown(wait=wait)

Expand Down
19 changes: 19 additions & 0 deletions pyee/trio.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ async def handler(data):
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.

Args:
nursery: Nursery that runs handlers. Mutually exclusive with
`manager`.
manager: Async context manager that yields a nursery, such as the
return value of `trio.open_nursery`. Mutually exclusive with
`nursery`. When both are omitted or `None`, a manager from
`trio.open_nursery` is stored.

Raises:
PyeeError: Both `nursery` and `manager` were passed.
"""

def __init__(
Expand Down Expand Up @@ -115,6 +126,14 @@ async def context(
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.

Returns:
This emitter, as an async context manager. Entering it yields
this emitter. A set nursery is used as-is. Otherwise the
manager is opened and its nursery is used.

Raises:
PyeeError: Neither a nursery nor a nursery manager is set.
"""
if self._nursery is not None:
yield self
Expand Down
27 changes: 26 additions & 1 deletion pyee/uplift.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@


def unwrap(event_emitter: EventEmitter) -> None:
"""Unwrap an uplifted EventEmitter, returning it to its prior state."""
"""Unwrap an uplifted EventEmitter, returning it to its prior state.

Args:
event_emitter: Emitter to restore. No effect when this emitter is
not uplifted.
"""
if event_emitter in EMIT_WRAPPERS:
EMIT_WRAPPERS[event_emitter]()

Expand Down Expand Up @@ -157,6 +162,26 @@ def uplift(
underlying event emitter should inherit from `EventEmitter`, or at
least implement the interface for the undocumented `_call_handlers` and
`_emit_handle_potential_error` methods.

Args:
cls: Event emitter class to instantiate.
underlying: Emitter whose handlers and events are linked with the
new instance.
error_handling: Which side handles an event that nobody listened
for. `"new"`, `"underlying"`, or `"neither"`. Defaults to
`"new"`.
proxy_new_listener: How `new_listener` events are copied.
`"forward"`, `"backward"`, `"both"`, or `"neither"`. Defaults
to `"forward"`.
args: Extra positional arguments passed to `cls`.
kwargs: Extra keyword arguments passed to `cls`.

Returns:
The new instance of `cls`.

Raises:
KeyError: `error_handling` or `proxy_new_listener` is not one of
the values listed above.
"""

(
Expand Down
Loading