Skip to content

Commit

Permalink
Class Decorator API (#84)
Browse files Browse the repository at this point in the history
* Add class decorator API

* Add autofunctions for pyee.cls

* Remove travis file

* docs: Fix a few typos (#91)

* Type Annotations (#97)

* Set up virtualenv, pyright and isort

* Run isort

* Passing type annotations for base.py

* action to run type checks

* Alas!

* Happy type checker for trio

* MOST of the library is type-checking

* working, non-cranky type annotations for uplift laul

* Type check the tests, cause an explosion

* Clean up requirements.txt

* tests type-checking

* py.typed file

* tests and linting happy

* Update build

* obvious action bugfix

* trailing comma

* remove inconsequential and angry type annotation

* Ignore type issues w asyncio import

* messy typecast

* anyway thats when I started blasting

* carnage!

* uplift bugfixes

* update pytest

* bye 3.6

* type annotations for cls

Co-authored-by: Tim Gates <tim.gates@iress.com>
  • Loading branch information
jfhbrook and timgates42 committed Jan 12, 2022
1 parent 049dbdc commit 2e212b0
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 0 deletions.
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
112 changes: 112 additions & 0 deletions pyee/cls.py
@@ -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
@@ -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 2e212b0

Please sign in to comment.