diff --git a/doc/source/reference/dispatch.rst b/doc/source/reference/dispatch.rst index 9e1c4fe..ad127ef 100644 --- a/doc/source/reference/dispatch.rst +++ b/doc/source/reference/dispatch.rst @@ -14,3 +14,19 @@ Event class .. autoclass:: pydispatch.dispatch.Event :members: + + +Exceptions +---------- + +.. autoclass:: pydispatch.dispatch.DoesNotExistError + :members: + +.. autoclass:: pydispatch.dispatch.ExistsError + :members: + +.. autoclass:: pydispatch.dispatch.EventExistsError + :members: + +.. autoclass:: pydispatch.dispatch.PropertyExistsError + :members: diff --git a/pydispatch/__init__.py b/pydispatch/__init__.py index 53d1449..7ff4b3d 100644 --- a/pydispatch/__init__.py +++ b/pydispatch/__init__.py @@ -15,5 +15,5 @@ 'After version 0.1.x, `python-dispatch` will only support Python 3.6 or greater.', UserWarning) -from pydispatch.dispatch import Dispatcher, Event +from pydispatch.dispatch import * from pydispatch.properties import * diff --git a/pydispatch/dispatch.py b/pydispatch/dispatch.py index f29d73c..fce58ff 100644 --- a/pydispatch/dispatch.py +++ b/pydispatch/dispatch.py @@ -9,6 +9,51 @@ import asyncio from pydispatch.aioutils import AioWeakMethodContainer, AioEventWaiters +__all__ = ( + 'DoesNotExistError', 'ExistsError', 'EventExistsError', + 'PropertyExistsError', 'Event', 'Dispatcher', +) + + +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' + + +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' + +class EventExistsError(ExistsError): + """Raised when registering an event name that already exists + as an :class:`Event` + + .. versionadded:: 0.2.2 + """ + + +class PropertyExistsError(ExistsError): + """Raised when registering an event name that already exists + as a :class:`~.properties.Property` + + .. versionadded:: 0.2.2 + """ class Event(object): @@ -108,10 +153,20 @@ def register_event(self, *names): 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: - continue + raise EventExistsError(name) + elif name in self.__property_events: + raise PropertyExistsError(name) self.__events[name] = Event(name) def bind(self, **kwargs): """Subscribes to events or to :class:`~pydispatch.properties.Property` updates @@ -164,6 +219,14 @@ class Foo(Dispatcher): 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 """ @@ -174,7 +237,10 @@ class Foo(Dispatcher): if name in props: e = props[name] else: - e = events[name] + try: + e = events[name] + except KeyError: + raise DoesNotExistError(name) e.add_listener(cb, __aio_loop__=aio_loop) def unbind(self, *args): """Unsubscribes from events or :class:`~pydispatch.properties.Property` updates @@ -224,10 +290,21 @@ def emit(self, name, *args, **kwargs): 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: - e = self.__events[name] + try: + e = self.__events[name] + except KeyError: + raise DoesNotExistError(name) return e(*args, **kwargs) def get_dispatcher_event(self, name): """Retrieves an Event object by name @@ -239,11 +316,21 @@ def get_dispatcher_event(self, name): 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: - e = self.__events[name] + try: + e = self.__events[name] + except KeyError: + raise DoesNotExistError(name) return e def emission_lock(self, name): """Holds emission of events and dispatches the last event on release @@ -279,9 +366,14 @@ def emission_lock(self, name): 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.__property_events.get(name) - if e is None: - e = self.__events[name] + e = self.get_dispatcher_event(name) return e.emission_lock diff --git a/tests/test_events.py b/tests/test_events.py index 241bee0..8f78a13 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -1,3 +1,4 @@ +import pytest def test_basic(listener, sender): sender.register_event('on_test_a') @@ -179,3 +180,52 @@ def test_emission_lock(listener, sender): sender.emit('on_test', 'inner') assert len(listener.received_event_data) == 1 assert listener.received_event_data[0]['args'] == ('inner', ) + + +def test_bind_and_emit_unregistered(): + from pydispatch import Dispatcher, DoesNotExistError + + class Sender(Dispatcher): + pass + + def callback(*args, **kwargs): + pass + + sender = Sender() + with pytest.raises(DoesNotExistError) as excinfo: + sender.bind(foo=callback) + assert '"foo"' in str(excinfo.value) + + with pytest.raises(DoesNotExistError) as excinfo: + sender.emit('foo') + assert '"foo"' in str(excinfo.value) + + with pytest.raises(DoesNotExistError) as excinfo: + e = sender.get_dispatcher_event('foo') + assert '"foo"' in str(excinfo.value) + + with pytest.raises(DoesNotExistError) as excinfo: + lock = sender.emission_lock('foo') + assert '"foo"' in str(excinfo.value) + +def test_register_existing_event(): + from pydispatch import Dispatcher, EventExistsError + + class Sender(Dispatcher): + _events_ = ['on_foo'] + + sender = Sender() + with pytest.raises(EventExistsError) as excinfo: + sender.register_event('on_foo') + assert '"on_foo"' in str(excinfo.value) + +def test_register_existing_property(): + from pydispatch import Dispatcher, Property, PropertyExistsError + + class Sender(Dispatcher): + foo = Property() + + sender = Sender() + with pytest.raises(PropertyExistsError) as excinfo: + sender.register_event('foo') + assert '"foo"' in str(excinfo.value)