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

Add DoesNotExistError and ExistsError #21

Merged
merged 4 commits into from
May 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions doc/source/reference/dispatch.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:
2 changes: 1 addition & 1 deletion pydispatch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
106 changes: 99 additions & 7 deletions pydispatch/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

"""
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
50 changes: 50 additions & 0 deletions tests/test_events.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pytest

def test_basic(listener, sender):
sender.register_event('on_test_a')
Expand Down Expand Up @@ -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)