Source code for pydispatch.dispatch

import types

from pydispatch.utils import (
    WeakMethodContainer,
    EmissionHoldLock,
    iscoroutinefunction,
)
from pydispatch.properties import Property
import asyncio
from pydispatch.aioutils import AioWeakMethodContainer, AioEventWaiters

__all__ = (
    'DoesNotExistError', 'ExistsError', 'EventExistsError',
    'PropertyExistsError', 'Event', 'Dispatcher',
)


[docs]class DoesNotExistError(KeyError): """Raised when binding to an :class:`Event` or :class:`~.properties.Property` that does not exist .. versionadded:: 0.2.2 """ def __init__(self, name): self.name = name def __str__(self): return f'Event "{self.name}" not registered'
[docs]class ExistsError(RuntimeError): """Raised when registering an event name that already exists as either a normal :class:`Event` or :class:`~.properies.Property` .. versionadded:: 0.2.2 """ def __init__(self, name): self.name = name def __str__(self): return f'"{self.name}" already exists'
[docs]class EventExistsError(ExistsError): """Raised when registering an event name that already exists as an :class:`Event` .. versionadded:: 0.2.2 """
[docs]class PropertyExistsError(ExistsError): """Raised when registering an event name that already exists as a :class:`~.properties.Property` .. versionadded:: 0.2.2 """
[docs]class Event(object): """Holds references to event names and subscribed listeners This is used internally by :class:`Dispatcher`. """ __slots__ = ('name', 'listeners', 'aio_waiters', 'aio_listeners', 'emission_lock') def __init__(self, name): self.name = name self.listeners = WeakMethodContainer() self.aio_listeners = AioWeakMethodContainer() self.aio_waiters = AioEventWaiters() self.emission_lock = EmissionHoldLock(self) def add_listener(self, callback, **kwargs): if iscoroutinefunction(callback): loop = kwargs.get('__aio_loop__') if loop is None: raise RuntimeError('Coroutine function given without event loop') self.aio_listeners.add_method(loop, callback) else: self.listeners.add_method(callback) def remove_listener(self, obj): if isinstance(obj, (types.MethodType, types.FunctionType)): self.listeners.del_method(obj) self.aio_listeners.del_method(obj) else: self.listeners.del_instance(obj) self.aio_listeners.del_instance(obj)
[docs] def __call__(self, *args, **kwargs): """Dispatches the event to listeners Called by :meth:`~Dispatcher.emit` """ if self.emission_lock.held: self.emission_lock.last_event = (args, kwargs) return self.aio_waiters(*args, **kwargs) self.aio_listeners(*args, **kwargs) for m in self.listeners.iter_methods(): r = m(*args, **kwargs) if r is False: return r
def __await__(self): return self.aio_waiters.__await__() def __repr__(self): return '<{}: {}>'.format(self.__class__, self) def __str__(self): return self.name
[docs]class Dispatcher(object): """Core class used to enable all functionality in the library Interfaces with :class:`Event` and :class:`~pydispatch.properties.Property` objects upon instance creation. Events can be created by calling :meth:`register_event` or by the subclass definition:: class Foo(Dispatcher): _events_ = ['awesome_event', 'on_less_awesome_event'] Once defined, an event can be dispatched to listeners by calling :meth:`emit`. """ def __init_subclass__(cls, *args, **kwargs): super().__init_subclass__() props = getattr(cls, '_PROPERTIES_', {}).copy() events = getattr(cls, '_EVENTS_', set()).copy() for key, val in cls.__dict__.items(): if key == '_events_': events |= set(val) elif isinstance(val, Property): props[key] = val cls._PROPERTIES_ = props cls._EVENTS_ = events def __new__(cls, *args, **kwargs): obj = super().__new__(cls) obj._Dispatcher__init_events() return obj def __init__(self, *args, **kwargs): # Everything is handled by __new__ # This is only here to prevent exceptions being raised pass def __init_events(self): if hasattr(self, '_Dispatcher__events'): return self.__events = {} for name in self._EVENTS_: self.__events[name] = Event(name) self.__property_events = {} for name, prop in self._PROPERTIES_.items(): self.__property_events[name] = Event(name) prop._add_instance(self)
[docs] def register_event(self, *names): """Registers new events after instance creation Args: *names (str): Name or names of the events to register Raises: EventExistsError: If an event with the given name already exists PropertyExistsError: If a property with the given name already exists .. versionchanged:: 0.2.2 :class:`ExistsError` exceptions are raised when attempting to register an event or property that already exists """ for name in names: if name in self.__events: raise EventExistsError(name) elif name in self.__property_events: raise PropertyExistsError(name) self.__events[name] = Event(name)
[docs] def bind(self, **kwargs): """Subscribes to events or to :class:`~pydispatch.properties.Property` updates Keyword arguments are used with the Event or Property names as keys and the callbacks as values:: class Foo(Dispatcher): name = Property() foo = Foo() foo.bind(name=my_listener.on_foo_name_changed) foo.bind(name=other_listener.on_name, value=other_listener.on_value) The callbacks are stored as weak references and their order is not maintained relative to the order of binding. **Async Callbacks**: Callbacks may be :term:`coroutine functions <coroutine function>` (defined using :keyword:`async def` or decorated with :func:`@asyncio.coroutine <asyncio.coroutine>`), but an event loop must be explicitly provided with the keyword argument ``"__aio_loop__"`` (an instance of :class:`asyncio.BaseEventLoop`) >>> import asyncio >>> class Foo(Dispatcher): ... _events_ = ['test_event'] >>> class Bar(object): ... def __init__(self): ... self.got_foo_event = asyncio.Event() ... async def wait_for_foo(self): ... await self.got_foo_event.wait() ... print('got foo!') ... async def on_foo_test_event(self, *args, **kwargs): ... self.got_foo_event.set() >>> loop = asyncio.get_event_loop() >>> foo = Foo() >>> bar = Bar() >>> foo.bind(test_event=bar.on_foo_test_event, __aio_loop__=loop) >>> fut = asyncio.ensure_future(bar.wait_for_foo()) >>> foo.emit('test_event') >>> loop.run_until_complete(fut) got foo! This can also be done using :meth:`bind_async`. Raises: DoesNotExistError: If attempting to bind to an event or property that has not been registered .. versionchanged:: 0.2.2 :class:`DoesNotExistError` is now raised when binding to non-existent events or properties .. versionadded:: 0.1.0 """ aio_loop = kwargs.pop('__aio_loop__', None) props = self.__property_events events = self.__events for name, cb in kwargs.items(): if name in props: e = props[name] else: try: e = events[name] except KeyError: raise DoesNotExistError(name) e.add_listener(cb, __aio_loop__=aio_loop)
[docs] def unbind(self, *args): """Unsubscribes from events or :class:`~pydispatch.properties.Property` updates Multiple arguments can be given. Each of which can be either the method that was used for the original call to :meth:`bind` or an instance object. If an instance of an object is supplied, any previously bound Events and Properties will be 'unbound'. """ props = self.__property_events.values() events = self.__events.values() for arg in args: for prop in props: prop.remove_listener(arg) for e in events: e.remove_listener(arg)
[docs] def bind_async(self, loop, **kwargs): """Subscribes to events with async callbacks Functionality is matches the :meth:`bind` method, except the provided callbacks should be coroutine functions. When the event is dispatched, callbacks will be placed on the given event loop. For keyword arguments, see :meth:`bind`. Args: loop: The :class:`EventLoop <asyncio.BaseEventLoop>` to use when events are dispatched Availability: Python>=3.5 .. versionadded:: 0.1.0 """ kwargs['__aio_loop__'] = loop self.bind(**kwargs)
[docs] def emit(self, name, *args, **kwargs): """Dispatches an event to any subscribed listeners Note: If a listener returns :obj:`False`, the event will stop dispatching to other listeners. Any other return value is ignored. Args: name (str): The name of the :class:`Event` to dispatch *args (Optional): Positional arguments to be sent to listeners **kwargs (Optional): Keyword arguments to be sent to listeners Raises: DoesNotExistError: If attempting to emit an event or property that has not been registered .. versionchanged:: 0.2.2 :class:`DoesNotExistError` is now raised if the event or property does not exist """ e = self.__property_events.get(name) if e is None: try: e = self.__events[name] except KeyError: raise DoesNotExistError(name) return e(*args, **kwargs)
[docs] def get_dispatcher_event(self, name): """Retrieves an Event object by name Args: name (str): The name of the :class:`Event` or :class:`~pydispatch.properties.Property` object to retrieve Returns: The :class:`Event` instance for the event or property definition Raises: DoesNotExistError: If no event or property with the given name exists .. versionchanged:: 0.2.2 :class:`DoesNotExistError` is now raised if the event or property does not exist .. versionadded:: 0.1.0 """ e = self.__property_events.get(name) if e is None: try: e = self.__events[name] except KeyError: raise DoesNotExistError(name) return e
[docs] def emission_lock(self, name): """Holds emission of events and dispatches the last event on release The context manager returned will store the last event data called by :meth:`emit` and prevent callbacks until it exits. On exit, it will dispatch the last event captured (if any) >>> class Foo(Dispatcher): ... _events_ = ['my_event'] >>> def on_my_event(value): ... print(value) >>> foo = Foo() >>> foo.bind(my_event=on_my_event) >>> with foo.emission_lock('my_event'): ... foo.emit('my_event', 1) ... foo.emit('my_event', 2) 2 Args: name (str): The name of the :class:`Event` or :class:`~pydispatch.properties.Property` Returns: A context manager to be used by the :keyword:`with` statement. If available, this will also be an async context manager to be used with the :keyword:`async with` statement (see `PEP 492`_). Note: The context manager is re-entrant, meaning that multiple calls to this method within nested context scopes are possible. Raises: DoesNotExistError: If no event or property with the given name exists .. versionchanged:: 0.2.2 :class:`DoesNotExistError` is now raised if the event or property does not exist .. _PEP 492: https://www.python.org/dev/peps/pep-0492/#asynchronous-context-managers-and-async-with """ e = self.get_dispatcher_event(name) return e.emission_lock
class _GlobalDispatcher(Dispatcher): def _has_event(self, name): try: self.get_dispatcher_event(name) except KeyError: return False return True _GLOBAL_DISPATCHER = _GlobalDispatcher()