Skip to content

Commit

Permalink
Add class decorator API
Browse files Browse the repository at this point in the history
  • Loading branch information
jfhbrook committed Aug 15, 2021
1 parent bca29b4 commit 7c0eedb
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Makefile
Expand Up @@ -12,7 +12,7 @@ package:
upload:
twine upload dist/*

test: lint
test:
pytest ./tests

tox:
Expand Down
104 changes: 104 additions & 0 deletions pyee/cls.py
@@ -0,0 +1,104 @@
from collections import namedtuple
from functools import wraps

from pyee import EventEmitter

EventHandler = namedtuple("Event", field_names=["event", "method"])


class Handlers:
def __init__(self):
self.reset()

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):
"""
Register an event handler on an evented class. See the ``evented`` class
decorator for a full example.
"""

def decorator(method):
_handlers.append(EventHandler(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


def evented(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(_handlers)
_handlers.reset()

og_init = 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
@@ -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()

0 comments on commit 7c0eedb

Please sign in to comment.