Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ee.event_names #99

Merged
merged 4 commits into from
Jan 12, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ API Docs:

.. autofunction:: pyee.uplift.uplift

.. autofunction:: pyee.cls.on

.. autofunction:: pyee.cls.evented


Some Links
==========
Expand Down
18 changes: 13 additions & 5 deletions pyee/base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-

from collections import defaultdict, OrderedDict
from collections import OrderedDict
from threading import Lock
from typing import Any, Callable, Dict, List, Optional, Tuple
from typing import Any, Callable, Dict, List, Optional, Set, Tuple


class PyeeException(Exception):
Expand Down Expand Up @@ -44,7 +44,7 @@ def __init__(self) -> None:
self._events: Dict[
str,
"OrderedDict[Callable, Callable]",
] = defaultdict(OrderedDict)
] = dict()
self._lock: Lock = Lock()

def on(self, event: str, f: Optional[Callable] = None) -> Callable:
Expand Down Expand Up @@ -110,6 +110,8 @@ def _add_event_handler(self, event: str, k: Callable, v: Callable):
# 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(
Expand All @@ -120,6 +122,10 @@ def _emit_run(
) -> None:
f(*args, **kwargs)

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

def _emit_handle_potential_error(self, event: str, error: Any) -> None:
if event == "error":
if isinstance(error, Exception):
Expand All @@ -136,7 +142,7 @@ def _call_handlers(
handled = False

with self._lock:
funcs = list(self._events[event].values())
funcs = list(self._events.get(event, OrderedDict()).values())
for f in funcs:
self._emit_run(f, args, kwargs)
handled = True
Expand Down Expand Up @@ -203,6 +209,8 @@ def g(
def _remove_listener(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:
"""Removes the function ``f`` from ``event``."""
Expand All @@ -217,7 +225,7 @@ def remove_all_listeners(self, event: Optional[str] = None) -> None:
if event is not None:
self._events[event] = OrderedDict()
else:
self._events = defaultdict(OrderedDict)
self._events = dict()

def listeners(self, event: str) -> List[Callable]:
"""Returns a list of all listeners registered to the ``event``."""
Expand Down
112 changes: 112 additions & 0 deletions pyee/cls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from dataclasses import dataclass
from functools import wraps
from typing import Callable, List, Type, TypeVar

from pyee import EventEmitter


@dataclass
class Handler:
event: str
method: Callable


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

def append(self, handler):
self._handlers.append(handler)

def __iter__(self):
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, method):
@wraps(method)
def bound(*args, **kwargs):
return method(self, *args, **kwargs)

return bound


Cls = TypeVar(name="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::

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``::

@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, *args, **kwargs):
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
47 changes: 47 additions & 0 deletions tests/test_cls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
from mock import Mock
import pytest

from pyee import EventEmitter
from pyee.cls import evented, on


@evented
class EventedFixture:
def __init__(self):
self.call_me = Mock()

@on("event")
def event_handler(self, *args, **kwargs):
self.call_me(self, *args, **kwargs)


_custom_event_emitter = EventEmitter()


@evented
class CustomEmitterFixture:
def __init__(self):
self.call_me = Mock()
self.event_emitter = _custom_event_emitter

@on("event")
def event_handler(self, *args, **kwargs):
self.call_me(self, *args, **kwargs)


class InheritedFixture(EventedFixture):
pass


@pytest.mark.parametrize(
"cls", [EventedFixture, CustomEmitterFixture, InheritedFixture]
)
def test_evented_decorator(cls):
inst = cls()

inst.event_emitter.emit("event", "emitter is emitted!")

inst.call_me.assert_called_once_with(inst, "emitter is emitted!")

_custom_event_emitter.remove_all_listeners()
26 changes: 23 additions & 3 deletions tests/test_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ def event_handler(data, **kwargs):
call_me()
assert data == "emitter is emitted!"

assert ee.event_names() == {"event"}

# Making sure data is passed propers
ee.emit("event", "emitter is emitted!", error=False)

Expand All @@ -43,6 +45,8 @@ def test_emit_error():
def on_error(exc):
call_me()

assert ee.event_names() == {"error"}

# No longer raises and error instead return True indicating handled
assert ee.emit("error", test_exception) is True
call_me.assert_called_once()
Expand All @@ -56,6 +60,8 @@ def test_emit_return():
call_me = Mock()
ee = EventEmitter()

assert ee.event_names() == set()

# make sure emitting without a callback returns False
assert not ee.emit("data")

Expand All @@ -79,6 +85,8 @@ def test_new_listener_event():
def event_handler(data):
pass

assert ee.event_names() == {"new_listener", "event"}

call_me.assert_called_once_with("event", event_handler)


Expand Down Expand Up @@ -106,6 +114,8 @@ def fourth():

ee.on("event", fourth)

assert ee.event_names() == {"event"}

assert ee._events["event"] == OrderedDict(
[(first, first), (second, second), (third, third), (fourth, fourth)]
)
Expand All @@ -120,7 +130,7 @@ def fourth():
assert ee._events["event"] == OrderedDict([(third, third), (fourth, fourth)])

ee.remove_all_listeners("event")
assert ee._events["event"] == OrderedDict()
assert "event" not in ee._events["event"]


def test_listener_removal_on_emit():
Expand All @@ -137,6 +147,8 @@ def should_remove():
ee.on("remove", should_remove)
ee.on("remove", call_me)

assert ee.event_names() == {"remove"}

ee.emit("remove")

call_me.assert_called_once()
Expand All @@ -148,6 +160,8 @@ def should_remove():
ee.on("remove", call_me)
ee.on("remove", should_remove)

assert ee.event_names() == {"remove"}

ee.emit("remove")

call_me.assert_called_once()
Expand All @@ -169,11 +183,15 @@ def once_handler(data):
# Tests to make sure that after event is emitted that it's gone.
ee.once("event", once_handler)

assert ee.event_names() == {"event"}

ee.emit("event", "emitter is emitted!")

call_me.assert_called_once()

assert ee._events["event"] == OrderedDict()
assert ee.event_names() == set()

assert "event" not in ee._events


def test_once_removal():
Expand All @@ -187,10 +205,12 @@ def once_handler(data):
handle = ee.once("event", once_handler)

assert handle == once_handler
assert ee.event_names() == {"event"}

ee.remove_listener("event", handle)

assert ee._events["event"] == OrderedDict()
assert "event" not in ee._events
assert ee.event_names() == set()


def test_listeners():
Expand Down