Skip to content
Merged
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
7 changes: 4 additions & 3 deletions .github/workflows/qa.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ jobs:
run: |
flake8 ./pyee ./tests
validate-pyproject ./pyproject.toml
- name: Run type checking
run: |
npx pyright@latest
- name: Run pyright
run: npx pyright@latest
- name: Run mypy
run: mypy .
- name: Run tests
run: pytest ./tests

Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## 2025/03/17 Version 13.0.0

- Type checking improvements
- Introduce overloads for `ee.on`
- Add `None` return type for functions as appropriate
- Type `self` as `Any` in all methods
- Local and CI tasks for type checking with `mypy`
- `mypy` type checking passes
- `pyright` type checking passes
- Addition of `mypy` to development dependencies
- Removed conditional import of `iscoroutine`
- This was implemented to support Python 3.3, which was dropped long ago
- Removed type stub for `twisted.python.Failure`
- This was to address a typing issue in unsupported versions of Twisted
- Export `Handler` type in `pyee/__init__.py`

## 2024/11/16 Version 12.1.1

- Fixed ReadTheDocs build
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ Listed in no particular order:
- Tim Gates <[email protected]>
- @asellappen
- @ddelange
- Yuichiro Tachibana @whitphx
4 changes: 4 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ lint:
check:
. ./venv/bin/activate && npx pyright@latest

# Check type annotations with mypy
mypy:
. ./venv/bin/activate && mypy .

# Run tests with pytest
test:
. ./venv/bin/activate && pytest ./tests
Expand Down
2 changes: 2 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[mypy]
exclude = ^venv/|^docs/
4 changes: 2 additions & 2 deletions pyee/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@
"""

from pyee.base import EventEmitter, PyeeError, PyeeException
from pyee.base import EventEmitter, Handler, PyeeError, PyeeException

__all__ = ["EventEmitter", "PyeeError", "PyeeException"]
__all__ = ["EventEmitter", "Handler", "PyeeError", "PyeeException"]
20 changes: 11 additions & 9 deletions pyee/asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

from pyee.base import EventEmitter

Self = Any

__all__ = ["AsyncIOEventEmitter"]


Expand Down Expand Up @@ -36,13 +38,13 @@ async def async_handler(*args, **kwargs):
coroutine is scheduled in a fire-and-forget fashion.
"""

def __init__(self, loop: Optional[AbstractEventLoop] = None):
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: Self,
event: str,
*args: Any,
**kwargs: Any,
Expand All @@ -68,11 +70,11 @@ def emit(
return super().emit(event, *args, **kwargs)

def _emit_run(
self,
self: Self,
f: Callable,
args: Tuple[Any, ...],
kwargs: Dict[str, Any],
):
) -> None:
try:
coro: Any = f(*args, **kwargs)
except Exception as exc:
Expand All @@ -92,20 +94,20 @@ def _emit_run(
else:
return

def callback(f):
def callback(f: Future) -> None:
self._waiting.discard(f)

if f.cancelled():
return

exc: Exception = f.exception()
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):
async def wait_for_complete(self: Self) -> None:
"""Waits for all pending tasks to complete. For example:

```py
Expand All @@ -128,7 +130,7 @@ async def async_handler(*args, **kwargs):
if self._waiting:
await wait(self._waiting)

def cancel(self):
def cancel(self: Self) -> None:
"""Cancel all pending tasks. For example:

```py
Expand All @@ -153,7 +155,7 @@ async def async_handler(*args, **kwargs):
self._waiting.clear()

@property
def complete(self) -> bool:
def complete(self: Self) -> bool:
"""When true, there are no pending tasks, and execution is complete.
For example:

Expand Down
42 changes: 25 additions & 17 deletions pyee/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@
List,
Mapping,
Optional,
overload,
Set,
Tuple,
TypeVar,
Union,
)

Self = Any


class PyeeException(Exception):
"""An exception internal to pyee. Deprecated in favor of PyeeError."""
Expand Down Expand Up @@ -56,24 +59,29 @@ def on_error(message):
your own exceptions, and treat them accordingly.
"""

def __init__(self) -> None:
def __init__(self: Self) -> None:
self._events: Dict[
str,
"OrderedDict[Callable, Callable]",
] = dict()
self._lock: Lock = Lock()

def __getstate__(self) -> Mapping[str, Any]:
def __getstate__(self: Self) -> Mapping[str, Any]:
state = self.__dict__.copy()
del state["_lock"]
return state

def __setstate__(self, state: Mapping[str, Any]) -> None:
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, event: str, f: Optional[Handler] = None
self: Self, event: str, f: Optional[Handler] = None
) -> Union[Handler, Callable[[Handler], Handler]]:
"""Registers the function `f` to the event name `event`, if provided.

Expand Down Expand Up @@ -106,7 +114,7 @@ def data_handler(data):
else:
return self.add_listener(event, f)

def listens_to(self, event: str) -> Callable[[Handler], Handler]:
def listens_to(self: Self, event: str) -> Callable[[Handler], Handler]:
"""Returns a decorator which will register the decorated function to
the event name `event`:

Expand All @@ -126,7 +134,7 @@ def on(f: Handler) -> Handler:

return on

def add_listener(self, event: str, f: Handler) -> Handler:
def add_listener(self: Self, event: str, f: Handler) -> Handler:
"""Register the function `f` to the event name `event`:

```
Expand All @@ -142,7 +150,7 @@ def data_handler(data):
self._add_event_handler(event, f, f)
return f

def _add_event_handler(self, event: str, k: Callable, v: Callable):
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)

Expand All @@ -156,26 +164,26 @@ def _add_event_handler(self, event: str, k: Callable, v: Callable):
self._events[event][k] = v

def _emit_run(
self,
self: Self,
f: Callable,
args: Tuple[Any, ...],
kwargs: Dict[str, Any],
) -> None:
f(*args, **kwargs)

def event_names(self) -> Set[str]:
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, event: str, error: Any) -> None:
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: Self,
event: str,
args: Tuple[Any, ...],
kwargs: Dict[str, Any],
Expand All @@ -191,7 +199,7 @@ def _call_handlers(
return handled

def emit(
self,
self: Self,
event: str,
*args: Any,
**kwargs: Any,
Expand All @@ -217,7 +225,7 @@ def emit(
return handled

def once(
self,
self: Self,
event: str,
f: Optional[Callable] = None,
) -> Callable:
Expand Down Expand Up @@ -249,18 +257,18 @@ def g(
else:
return _wrapper(f)

def _remove_listener(self, event: str, f: Callable) -> None:
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, event: str, f: Callable) -> None:
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, event: Optional[str] = None) -> None:
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.
"""
Expand All @@ -270,6 +278,6 @@ def remove_all_listeners(self, event: Optional[str] = None) -> None:
else:
self._events = dict()

def listeners(self, event: str) -> List[Callable]:
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())
14 changes: 7 additions & 7 deletions pyee/cls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from dataclasses import dataclass
from functools import wraps
from typing import Callable, List, Type, TypeVar
from typing import Any, Callable, Iterator, List, Type, TypeVar

from pyee import EventEmitter

Expand All @@ -12,13 +12,13 @@ class Handler:


class Handlers:
def __init__(self):
def __init__(self) -> None:
self._handlers: List[Handler] = []

def append(self, handler):
def append(self, handler) -> None:
self._handlers.append(handler)

def __iter__(self):
def __iter__(self) -> Iterator[Handler]:
return iter(self._handlers)

def reset(self):
Expand All @@ -41,9 +41,9 @@ def decorator(method: Callable) -> Callable:
return decorator


def _bind(self, method):
def _bind(self: Any, method: Any) -> Any:
@wraps(method)
def bound(*args, **kwargs):
def bound(*args, **kwargs) -> Any:
return method(self, *args, **kwargs)

return bound
Expand Down Expand Up @@ -103,7 +103,7 @@ async def event_handler(self, *args, **kwargs):
og_init: Callable = cls.__init__

@wraps(cls.__init__)
def init(self, *args, **kwargs):
def init(self: Any, *args: Any, **kwargs: Any) -> None:
og_init(self, *args, **kwargs)
if not hasattr(self, "event_emitter"):
self.event_emitter = EventEmitter()
Expand Down
15 changes: 9 additions & 6 deletions pyee/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

from pyee.base import EventEmitter

Self = Any

__all__ = ["ExecutorEventEmitter"]


Expand Down Expand Up @@ -44,19 +46,19 @@ def handler(data):
No effort is made to ensure thread safety, beyond using an executor.
"""

def __init__(self, executor: Optional[Executor] = None):
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: Self,
f: Callable,
args: Tuple[Any, ...],
kwargs: Dict[str, Any],
):
) -> None:
future: Future = self._executor.submit(f, *args, **kwargs)

@future.add_done_callback
Expand All @@ -67,15 +69,16 @@ def _callback(f: Future) -> None:
elif exc is not None:
raise exc

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

self._executor.shutdown(wait=wait)

def __enter__(self) -> "ExecutorEventEmitter":
def __enter__(self: Self) -> "ExecutorEventEmitter":
return self

def __exit__(
self, type: Type[Exception], value: Exception, traceback: TracebackType
self: Self, type: Type[Exception], value: Exception, traceback: TracebackType
) -> Optional[bool]:
self.shutdown()
return None
Loading