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

Class Decorator API #84

Merged
merged 7 commits into from Jan 12, 2022
Merged
Show file tree
Hide file tree
Changes from 2 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
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
4 changes: 4 additions & 0 deletions docs/index.rst
Expand Up @@ -43,6 +43,10 @@ API Docs:

.. autofunction:: pyee.uplift.uplift

.. autofunction:: pyee.cls.on

.. autofunction:: pyee.cls.evented


Some Links
==========
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"])
jfhbrook marked this conversation as resolved.
Show resolved Hide resolved


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()
jfhbrook marked this conversation as resolved.
Show resolved Hide resolved


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__
jfhbrook marked this conversation as resolved.
Show resolved Hide resolved

@wraps(cls.__init__)
def init(self, *args, **kwargs):
og_init(self, *args, **kwargs)
if not hasattr(self, "event_emitter"):
self.event_emitter = EventEmitter()
jfhbrook marked this conversation as resolved.
Show resolved Hide resolved

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)
jfhbrook marked this conversation as resolved.
Show resolved Hide resolved


_custom_event_emitter = EventEmitter()
jfhbrook marked this conversation as resolved.
Show resolved Hide resolved


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