Skip to content

Commit

Permalink
publish parameterised signals
Browse files Browse the repository at this point in the history
  • Loading branch information
keis committed Nov 30, 2013
1 parent 687741a commit 0eac7f6
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 23 deletions.
60 changes: 44 additions & 16 deletions smoke.py
Expand Up @@ -64,6 +64,10 @@ def weak_method(*args, **kwargs):

def subscribers(obj, event):
'''Get a list of all subscribers to `event` on `obj`'''

if hasattr(event, '__get__'):
event = event.__get__(obj)

if not hasattr(obj, '_subscribers'):
obj._subscribers = defaultdict(list)

Expand All @@ -72,14 +76,36 @@ def subscribers(obj, event):

def subscribe(obj, event, subscriber):
'''Add a subscriber to `event` on `obj`'''

if hasattr(event, '__get__'):
event = event.__get__(obj)

subscribers(obj, event).append(subscriber)


def disconnect(obj, event, subscriber):
'''Disconnect a subscriber to `event` on `obj`'''

if hasattr(event, '__get__'):
event = event.__get__(obj)

subscribers(obj, event).remove(subscriber)


def variants(event):
'''Get a generator that yields variations of a event.'''

if hasattr(event, 'parameters'):
parent = event.parent
l = len(event.parameters)
for i in range(l+1):
yield parent.parameterise(event.parameters[:l-i])[0]
else:
yield event
if hasattr(event, 'name'):
yield event.name


def _publish(obj, _event, **kwargs):
'''Invoke all subscribers to `event` on `obj`
Expand All @@ -94,19 +120,21 @@ def _publish(obj, _event, **kwargs):
All other exceptions will be passed to the parent context and will
break the publish loop without notifing remaining subscribers
'''
subs = subscribers(obj, _event)
disconnected = []
try:
for sub in subs:
try:
sub(**kwargs)
except Disconnect:
disconnected.append(sub)
except StopPropagation:
break
finally:
for d in disconnected:
subs.remove(d)

for var in variants(_event):
subs = subscribers(obj, var)
disconnected = []
try:
for sub in subs:
try:
sub(**kwargs)
except Disconnect:
disconnected.append(sub)
except StopPropagation:
break
finally:
for d in disconnected:
subs.remove(d)


@wraps(_publish, ['__doc__'])
Expand Down Expand Up @@ -145,15 +173,15 @@ def __init__(self, signal, im_self):

def subscribe(self, subscriber):
'''Subscribe a callback to this event'''
subscribe(self.__im_self, self.__signal.name or self, subscriber)
subscribe(self.__im_self, self, subscriber)

def disconnect(self, subscriber):
'''Disconnect a callback from this event'''
disconnect(self.__im_self, self.__signal.name or self, subscriber)
disconnect(self.__im_self, self, subscriber)

def publish(self, **kwargs):
'''Publish this event on `obj`'''
publish(self.__im_self, self.__signal.name or self, **kwargs)
publish(self.__im_self, self, **kwargs)

def __call__(self, *args, **kwargs):
'''parameterise and publish'''
Expand Down
2 changes: 1 addition & 1 deletion tests/matchers.py
Expand Up @@ -4,7 +4,7 @@


class RaisesContext(object):
pass
exception = None


@contextmanager
Expand Down
12 changes: 6 additions & 6 deletions tests/test_mixed.py
Expand Up @@ -32,22 +32,22 @@ def test_subscribe_broker_publish_signal(self):

assert_that(self.listener.spam_cb, called_once_with(s=sentinel))

def test_subscribe_by_name(self):
def test_subscribe_broker_publish_signal_with_name(self):
sentinel = object()
self.mixed.subscribe('egg', self.listener.egg_cb)
self.mixed.subscribe(self.mixed.egg, self.listener.egg_cb)
self.mixed.egg(s=sentinel)

assert_that(self.listener.egg_cb, called_once_with(s=sentinel))

def test_publish_by_name(self):
def test_subscribe_by_name(self):
sentinel = object()
self.mixed.egg.subscribe(self.listener.egg_cb)
self.mixed.publish('egg', s=sentinel)
self.mixed.subscribe('egg', self.listener.egg_cb)
self.mixed.egg(s=sentinel)

assert_that(self.listener.egg_cb, called_once_with(s=sentinel))

def test_publish_override(self):
sentinel = object()
self.mixed.publish = mock.Mock(wraps=self.mixed.publish)
self.mixed.egg(s=sentinel)
assert_that(self.mixed.publish, called_once_with('egg', s=sentinel))
assert_that(self.mixed.publish, called_once_with(self.mixed.egg, s=sentinel))
62 changes: 62 additions & 0 deletions tests/test_parameterised.py
Expand Up @@ -74,3 +74,65 @@ class Foo(object):
assert_that(p, instance_of(boundsignal))
assert_that(p, is_not(equal_to(q)))
assert_that(p, equal_to(f.cname))


def test_disallows_publish_without_all_parameters():
change = signal('change', 'attribute', 'type')
p = change('name')
with assert_raises(instance_of(TypeError)):
p(Source())


def test_subscribe_one_generic():
s = Source()
l = mock.Mock()
sentinel = object()

change = signal('change', 'attribute', 'type')
p = change('name', 'knighted')
q = change('name')
q.subscribe(s, l)
p.publish(s, s=sentinel)

assert_that(l, called_once_with(s=sentinel))


def test_subscribe_exact():
s = Source()
l = mock.Mock()
sentinel = object()

change = signal('change', 'attribute')
p = change('name')
p.subscribe(s, l)
p.publish(s, s=sentinel)

assert_that(l, called_once_with(s=sentinel))


def test_subscribe_mismatch():
s = Source()
l = mock.Mock()
sentinel = object()

change = signal('change', 'attribute')
p = change('name')
q = change('age')
q.subscribe(s, l)
p.publish(s, s=sentinel)

assert_that(p, is_not(equal_to(q)))
assert_that(l, has_property('call_count', 0))


def test_subscribe_generic():
s = Source()
l = mock.Mock()
sentinel = object()

change = signal('change', 'attribute')
p = change('name')
change.subscribe(s, l)
p.publish(s, s=sentinel)

assert_that(l, called_once_with(s=sentinel))

0 comments on commit 0eac7f6

Please sign in to comment.