Skip to content

Commit

Permalink
Changed some event related terminology
Browse files Browse the repository at this point in the history
Renamed event_name to topic and fire_event() to dispatch().
  • Loading branch information
agronholm committed Sep 9, 2015
1 parent b6038a9 commit 4481dde
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 59 deletions.
4 changes: 2 additions & 2 deletions asphalt/core/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def run(self):
event_loop.run_until_complete(asyncio.gather(*coroutines))

# Run all the application context's start callbacks
event_loop.run_until_complete(context.fire_event('started'))
event_loop.run_until_complete(context.dispatch('started'))
self.logger.info('Application started')
except Exception as exc:
self.logger.exception('Error during application startup')
Expand All @@ -121,6 +121,6 @@ def run(self):
except (KeyboardInterrupt, SystemExit):
pass

event_loop.run_until_complete(context.fire_event('finished'))
event_loop.run_until_complete(context.dispatch('finished'))
event_loop.close()
self.logger.info('Application stopped')
18 changes: 9 additions & 9 deletions asphalt/core/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def __str__(self):

class ResourceEvent(Event):
"""
Fired when a resource has been added or removed to a context.
Dispatched when a resource has been added or removed to a context.
:ivar source: the relevant context
:ivar types: names of the types for the resource
Expand All @@ -52,9 +52,9 @@ class ResourceEvent(Event):

__slots__ = 'types', 'alias', 'lazy'

def __init__(self, source: 'Context', event_name: str, types: Tuple[str], alias: str,
def __init__(self, source: 'Context', topic: str, types: Tuple[str], alias: str,
lazy: bool):
super().__init__(source, event_name)
super().__init__(source, topic)
self.types = types
self.alias = alias
self.lazy = lazy
Expand Down Expand Up @@ -132,8 +132,8 @@ def __init__(self, scope: ContextScope, parent: 'Context'=None):

# Forward resource events from the parent(s)
if parent is not None:
parent.add_listener('resource_added', self._fire_event)
parent.add_listener('resource_removed', self._fire_event)
parent.add_listener('resource_added', self._dispatch)
parent.add_listener('resource_removed', self._dispatch)

def __getattr__(self, name):
creator = self._resource_creators.get(name)
Expand Down Expand Up @@ -189,15 +189,15 @@ def _add_resource(self, value, alias: str, context_var: str,
if creator is None and resource.context_var:
setattr(self, context_var, value)

self.fire_event('resource_added', types, alias, False)
self.dispatch('resource_added', types, alias, False)
return resource

@asynchronous
def add_resource(
self, value, alias: str='default', context_var: str=None, *,
types: Union[Union[str, type], Iterable[Union[str, type]]]=()) -> Resource:
"""
Adds a resource to the collection and fires a "resource_added" event.
Adds a resource to the collection and dispatches a "resource_added" event.
:param value: the actual resource value
:param alias: an identifier for this resource (unique among all its registered types)
Expand Down Expand Up @@ -239,7 +239,7 @@ def add_lazy_resource(self, creator: Callable[['Context'], Any],
@asynchronous
def remove_resource(self, resource: Resource):
"""
Removes the given resource from the collection and fires a "resource_removed" event.
Removes the given resource from the collection and dispatches a "resource_removed" event.
:param resource: the resource to be removed
:raises LookupError: the given resource was not in the collection
Expand All @@ -259,7 +259,7 @@ def remove_resource(self, resource: Resource):
if resource.context_var and resource.context_var in self.__dict__:
delattr(self, resource.context_var)

self.fire_event('resource_removed', resource.types, resource.alias, False)
self.dispatch('resource_removed', resource.types, resource.alias, False)

def _get_resource(self, resource_type: str, alias: str):
resource = self._resources.get(resource_type, {}).get(alias)
Expand Down
65 changes: 32 additions & 33 deletions asphalt/core/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,46 +18,45 @@ class Event:
"""
The base class for all events.
:ivar source: the object that (originally) fired this event
:ivar name: the event name
:ivar source: the object where this event originated from
:ivar topic: the topic
"""

__slots__ = 'source', 'name'
__slots__ = 'source', 'topic'

def __init__(self, source: 'EventSource', name: str):
def __init__(self, source: 'EventSource', topic: str):
self.source = source
self.name = name
self.topic = topic


class ListenerHandle:
__slots__ = 'event_name', 'callback', 'args', 'kwargs', 'priority'
__slots__ = 'topic', 'callback', 'args', 'kwargs', 'priority'

def __init__(self, event_name: str, callback: Callable[[Event], Any],
def __init__(self, topic: str, callback: Callable[[Event], Any],
args: Sequence[Any], kwargs: Dict[str, Any], priority: ListenerPriority):
self.event_name = event_name
self.topic = topic
self.callback = callback
self.args = args
self.kwargs = kwargs
self.priority = priority

def __lt__(self, other):
if isinstance(other, ListenerHandle):
return (self.event_name, self.priority.value) < (self.event_name, other.priority.value)
return (self.topic, self.priority.value) < (self.topic, other.priority.value)
return NotImplemented

def __repr__(self):
return ('ListenerHandle(event_name={0.event_name!r}, callback={1}, args={0.args!r}, '
return ('ListenerHandle(topic={0.topic!r}, callback={1}, args={0.args!r}, '
'kwargs={0.kwargs!r}, priority={0.priority.name})'.
format(self, qualified_name(self.callback)))


class EventSource:
"""
A mixin class that provides support for firing and listening to events.
It requires a mapping of supported event named to their respective Event classes as its
first argument.
A mixin class that provides support for dispatching and listening to events.
It requires a mapping of topics to their respective event classes as its first argument.
:param event_classes: a mapping of event name -> event class
:param event_classes: a mapping of topic -> event class
"""

__slots__ = '_event_classes', '_listener_handles'
Expand All @@ -68,31 +67,31 @@ def __init__(self, event_classes: Dict[str, Any], *args, **kwargs):
super().__init__(*args, **kwargs)

@asynchronous
def add_listener(self, event_name: str, callback: Callable[[Any], Any],
def add_listener(self, topic: str, callback: Callable[[Any], Any],
args: Sequence[Any]=(), kwargs: Dict[str, Any]=None, *,
priority: ListenerPriority=ListenerPriority.neutral) -> ListenerHandle:
"""
Starts listening to the events specified by ``event_name``. The callback (which can be
Starts listening to events specified by ``topic``. The callback (which can be
a coroutine function) will be called with a single argument (an :class:`Event` instance).
The exact event class used depends on the event class mappings given to the constructor.
It is possible to prioritize the listener to be called among the first or last in the
group by specifying an alternate :class:`ListenerPriority` value as ``priority``.
:param event_name: the event name to listen to
:param callback: a callable to call with the event object when the event is fired
:param topic: the topic to listen to
:param callback: a callable to call with the event object when the event is dispatched
:param args: positional arguments to call the callback with (in addition to the event)
:param kwargs: keyword arguments to call the callback with
:param priority: priority of the callback among other listeners of the same event
:return: a listener handle which can be used with :meth:`remove_listener` to unlisten
:raises ValueError: if the named event has not been registered in this event source
"""

if event_name not in self._event_classes:
raise ValueError('no such event registered: {}'.format(event_name))
if topic not in self._event_classes:
raise ValueError('no such topic registered: {}'.format(topic))

handle = ListenerHandle(event_name, callback, args, kwargs or {}, priority)
handles = self._listener_handles[event_name]
handle = ListenerHandle(topic, callback, args, kwargs or {}, priority)
handles = self._listener_handles[topic]
handles.append(handle)
handles.sort()
return handle
Expand All @@ -107,34 +106,34 @@ def remove_listener(self, handle: ListenerHandle):
"""

try:
self._listener_handles[handle.event_name].remove(handle)
self._listener_handles[handle.topic].remove(handle)
except (KeyError, ValueError):
raise ValueError('listener not found') from None

@asynchronous
def fire_event(self, event_name: str, *args, **kwargs) -> Task:
def dispatch(self, topic: str, *args, **kwargs) -> Task:
"""
Instantiates an event matching the given registered event name and calls all the
listeners in a separate task.
Instantiates an event matching the given topic and calls all the listeners in a separate
task.
:param event_name: the event name identifying the event class to use
:param topic: the topic
:param args: positional arguments to pass to the event class constructor
:param kwargs: keyword arguments to pass to the event class constructor
:return: a Task that completes when all the event listeners have been called
:raises ValueError: if the named event has not been registered in this event source
"""

event_class = self._event_classes.get(event_name)
event_class = self._event_classes.get(topic)
if event_class is None:
raise ValueError('no such event registered: {}'.format(event_name))
raise ValueError('no such topic registered: {}'.format(topic))

# Run call_listeners() in a separate task to avoid arbitrary exceptions from listeners
event = event_class(self, event_name, *args, **kwargs)
return async(self._fire_event(event))
event = event_class(self, topic, *args, **kwargs)
return async(self._dispatch(event))

@coroutine
def _fire_event(self, event: Event):
for handle in self._listener_handles[event.name]:
def _dispatch(self, event: Event):
for handle in self._listener_handles[event.topic]:
retval = handle.callback(event, *handle.args, **handle.kwargs)
if retval is not None:
yield from retval
30 changes: 15 additions & 15 deletions tests/test_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@


class DummyEvent(Event):
def __init__(self, source, name, *args, **kwargs):
super().__init__(source, name)
def __init__(self, source, topic, *args, **kwargs):
super().__init__(source, topic)
self.args = args
self.kwargs = kwargs

Expand All @@ -32,7 +32,7 @@ def test_lt_not_implemented(self, handle):

def test_repr(self, handle):
assert repr(handle) == (
"ListenerHandle(event_name='foo', "
"ListenerHandle(topic='foo', "
"callback=test_event.TestListenerHandle.handle.<locals>.<lambda>, args=(), kwargs={}, "
"priority=neutral)")

Expand All @@ -45,19 +45,19 @@ def source(self):
def test_add_listener(self, source):
handle = source.add_listener('event_a', lambda: None)
assert isinstance(handle, ListenerHandle)
assert handle.event_name == 'event_a'
assert handle.topic == 'event_a'

def test_add_listener_nonexistent_event(self, source):
exc = pytest.raises(ValueError, source.add_listener, 'foo', lambda: None)
assert str(exc.value) == 'no such event registered: foo'
assert str(exc.value) == 'no such topic registered: foo'

@pytest.mark.asyncio
@pytest.mark.parametrize('as_coroutine', [True, False], ids=['coroutine', 'normal'])
@pytest.mark.parametrize('event_name, results', [
@pytest.mark.parametrize('topic, results', [
('event_a', [((7, 3), {'x': 6}), ((6, 1), {'a': 5}), ((1, 2), {'a': 3})]),
('event_b', [((9, 4), {'c': 1}), ((4, 5), {'b': 4})])
], ids=['event_a', 'event_b'])
def test_fire_event(self, source: EventSource, event_name, results, as_coroutine):
def test_dispatch_event(self, source: EventSource, topic, results, as_coroutine):
"""
Tests that firing an event triggers the right listeners and respects the callback
priorities. It also makes sure that callbacks can be either coroutines or normal callables.
Expand All @@ -78,27 +78,27 @@ def callback(event: Event, *args, **kwargs):
source.add_listener('event_a', callback, [6, 1], {'a': 5},
priority=ListenerPriority.neutral)
source.add_listener('event_b', callback, [9, 4], {'c': 1}, priority=ListenerPriority.first)
yield from source.fire_event(event_name, 'x', 'y', a=1, b=2)
yield from source.dispatch(topic, 'x', 'y', a=1, b=2)
yield from trigger.wait()

assert len(events) == len(results)
for (event, args, kwargs), (expected_args, expected_kwargs) in zip(events, results):
assert event.source == source
assert event.name == event_name
assert event.topic == topic
assert event.args == ('x', 'y')
assert event.kwargs == {'a': 1, 'b': 2}
assert args == expected_args
assert kwargs == expected_kwargs

@pytest.mark.parametrize('event_name', ['event_a', 'foo'],
@pytest.mark.parametrize('topic', ['event_a', 'foo'],
ids=['existing_event', 'nonexistent_event'])
def test_remove_noexistent_listener(self, source, event_name):
handle = ListenerHandle(event_name, lambda: None, (), {}, ListenerPriority.neutral)
def test_remove_noexistent_listener(self, source, topic):
handle = ListenerHandle(topic, lambda: None, (), {}, ListenerPriority.neutral)
exc = pytest.raises(ValueError, source.remove_listener, handle)
assert str(exc.value) == 'listener not found'

@pytest.mark.asyncio
def test_fire_nonexistent_event(self, source):
def test_dispatch_nonexistent_topic(self, source):
with pytest.raises(ValueError) as exc:
yield from source.fire_event('blah')
assert str(exc.value) == 'no such event registered: blah'
yield from source.dispatch('blah')
assert str(exc.value) == 'no such topic registered: blah'

0 comments on commit 4481dde

Please sign in to comment.