summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjason kirtland <jek@discorporate.us>2011-03-14 17:12:59 -0400
committerjason kirtland <jek@discorporate.us>2011-03-14 17:12:59 -0400
commitd24501d9f717e930f059b38e3043790edaf35f0c (patch)
tree1342993ec69016644f50026c9d0060ef4cdc05f4
parent6096207fa513d8bd7518e9d29f47ea126ea6ed09 (diff)
downloadblinker-d24501d9f717e930f059b38e3043790edaf35f0c.tar.gz
Added Signal.receiver_connected and .receiver_disconnected per-Signal signals.
Deprecates the global 'receiver_connected' signal.
-rw-r--r--CHANGES11
-rw-r--r--blinker/_utilities.py15
-rw-r--r--blinker/base.py60
-rw-r--r--tests/test_signals.py139
4 files changed, 225 insertions, 0 deletions
diff --git a/CHANGES b/CHANGES
index 2058376..d6caecb 100644
--- a/CHANGES
+++ b/CHANGES
@@ -2,6 +2,17 @@
Blinker Changelog
=================
+
+Version 1.2
+-----------
+
+Unreleased.
+
+- Added Signal.receiver_connected and
+ Signal.receiver_disconnected per-Signal signals.
+- Deprecated the global 'receiver_connected' signal.
+- Verified Python 3.2 support (no changes needed!)
+
Version 1.1
-----------
diff --git a/blinker/_utilities.py b/blinker/_utilities.py
index 9e9e037..940275d 100644
--- a/blinker/_utilities.py
+++ b/blinker/_utilities.py
@@ -136,3 +136,18 @@ def callable_reference(object, callback=None):
elif hasattr(object, '__self__') and object.__self__ is not None:
return BoundMethodWeakref(target=object, on_delete=callback)
return annotatable_weakref(object, callback)
+
+
+class lazy_property(object):
+ """A @property that is only evaluated once."""
+
+ def __init__(self, deferred):
+ self._deferred = deferred
+ self.__doc__ = deferred.__doc__
+
+ def __get__(self, obj, cls):
+ if obj is None:
+ return self
+ value = self._deferred(obj)
+ setattr(obj, self._deferred.__name__, value)
+ return value
diff --git a/blinker/base.py b/blinker/base.py
index d2a9eff..17c6833 100644
--- a/blinker/base.py
+++ b/blinker/base.py
@@ -16,6 +16,7 @@ from blinker._utilities import (
contextmanager,
defaultdict,
hashable_identity,
+ lazy_property,
reference,
symbol,
)
@@ -33,6 +34,42 @@ class Signal(object):
#: without an additional import.
ANY = ANY
+ @lazy_property
+ def receiver_connected(self):
+ """Emitted after each :meth:`connect`.
+
+ The signal sender is the signal instance, and the :meth:`connect`
+ arguments are passed through: *receiver*, *sender*, and *weak*.
+
+ .. versionadded:: 1.2
+
+ """
+ return Signal(doc="Emitted after a receiver connects.")
+
+ @lazy_property
+ def receiver_disconnected(self):
+ """Emitted after :meth:`disconnect`.
+
+ The sender is the signal instance, and the :meth:`disconnect` arguments
+ are passed through: *receiver* and *sender*.
+
+ Note, this signal is emitted **only** when :meth:`disconnect` is
+ called explicitly.
+
+ The disconnect signal can not be emitted by an automatic disconnect
+ (due to a weakly referenced receiver or sender going out of scope),
+ as the receiver and/or sender instances are no longer available for
+ use at the time this signal would be emitted.
+
+ An alternative approach is available by subscribing to
+ :attr:`receiver_connected` and setting up a custom weakref cleanup
+ callback on weak receivers and senders.
+
+ .. versionadded:: 1.2
+
+ """
+ return Signal(doc="Emitted after a receiver disconnects.")
+
def __init__(self, doc=None):
"""
:param doc: optional. If provided, will be assigned to the signal's
@@ -99,6 +136,16 @@ class Signal(object):
del sender_ref
# broadcast this connection. if receivers raise, disconnect.
+ if ('receiver_connected' in self.__dict__ and
+ self.receiver_connected.receivers):
+ try:
+ self.receiver_connected.send(self,
+ receiver=receiver,
+ sender=sender,
+ weak=weak)
+ except:
+ self.disconnect(receiver, sender)
+ raise
if receiver_connected.receivers and self is not receiver_connected:
try:
receiver_connected.send(self,
@@ -273,6 +320,12 @@ class Signal(object):
receiver_id = hashable_identity(receiver)
self._disconnect(receiver_id, sender_id)
+ if ('receiver_disconnected' in self.__dict__ and
+ self.receiver_disconnected.receivers):
+ self.receiver_disconnected.send(self,
+ receiver=receiver,
+ sender=sender)
+
def _disconnect(self, receiver_id, sender_id):
if sender_id == ANY_ID:
if self._by_receiver.pop(receiver_id, False):
@@ -310,6 +363,13 @@ Sent by a :class:`Signal` after a receiver connects.
:keyword sender_arg: the sender to connect to
:keyword weak_arg: true if the connection to receiver_arg is a weak reference
+.. deprecated:: 1.2
+
+As of 1.2, individual signals have their own private
+:attr:`~Signal.receiver_connected` and
+:attr:`~Signal.receiver_disconnected` signals with a slightly simplified
+call signature. This global signal is planned to be removed in 1.6.
+
""")
diff --git a/tests/test_signals.py b/tests/test_signals.py
index 9a5a5ce..22bf397 100644
--- a/tests/test_signals.py
+++ b/tests/test_signals.py
@@ -16,6 +16,21 @@ def collect():
time.sleep(0.1)
+class Sentinel(list):
+ """A signal receipt accumulator."""
+
+ def make_receiver(self, key):
+ """Return a generic signal receiver function logging as *key*
+
+ When connected to a signal, appends (key, sender, kw) to the Sentinel.
+
+ """
+ def receiver(*sentby, **kw):
+ self.append((key, sentby[0], kw))
+ receiver.func_name = 'receiver_%s' % key
+ return receiver
+
+
def test_meta_connect():
sentinel = []
def meta_received(sender, **kw):
@@ -38,6 +53,130 @@ def test_meta_connect():
blinker.receiver_connected._clear_state()
+def _test_signal_signals(sender):
+ sentinel = Sentinel()
+ sig = blinker.Signal()
+
+ connected = sentinel.make_receiver('receiver_connected')
+ disconnected = sentinel.make_receiver('receiver_disconnected')
+ receiver1 = sentinel.make_receiver('receiver1')
+ receiver2 = sentinel.make_receiver('receiver2')
+
+ assert not sig.receiver_connected.receivers
+ assert not sig.receiver_disconnected.receivers
+ sig.receiver_connected.connect(connected)
+ sig.receiver_disconnected.connect(disconnected)
+
+ assert sig.receiver_connected.receivers
+ assert not sentinel
+
+ for receiver, weak in [(receiver1, True), (receiver2, False)]:
+ sig.connect(receiver, sender=sender, weak=weak)
+
+ expected = ('receiver_connected',
+ sig,
+ dict(receiver=receiver, sender=sender, weak=weak))
+
+ assert sentinel[-1] == expected
+
+ # disconnect from explicit sender
+ sig.disconnect(receiver1, sender=sender)
+
+ expected = ('receiver_disconnected',
+ sig,
+ dict(receiver=receiver1, sender=sender))
+ assert sentinel[-1] == expected
+
+ # disconnect from ANY and all senders (implicit disconnect signature)
+ sig.disconnect(receiver2)
+ assert sentinel[-1] == ('receiver_disconnected',
+ sig,
+ dict(receiver=receiver2, sender=blinker.ANY))
+
+
+def test_signal_signals_any_sender():
+ _test_signal_signals(blinker.ANY)
+
+
+def test_signal_signals_strong_sender():
+ _test_signal_signals("squiznart")
+
+
+def test_signal_weak_receiver_vanishes():
+ # non-edge-case path for weak receivers is exercised in the ANY sender
+ # test above.
+ sentinel = Sentinel()
+ sig = blinker.Signal()
+
+ connected = sentinel.make_receiver('receiver_connected')
+ disconnected = sentinel.make_receiver('receiver_disconnected')
+ receiver1 = sentinel.make_receiver('receiver1')
+ receiver2 = sentinel.make_receiver('receiver2')
+
+ sig.receiver_connected.connect(connected)
+ sig.receiver_disconnected.connect(disconnected)
+
+ # explicit disconnect on a weak does emit the signal
+ sig.connect(receiver1, weak=True)
+ sig.disconnect(receiver1)
+
+ assert len(sentinel) == 2
+ assert sentinel[-1][2]['receiver'] is receiver1
+
+ del sentinel[:]
+ sig.connect(receiver2, weak=True)
+ assert len(sentinel) == 1
+
+ del sentinel[:] # holds a ref to receiver2
+ del receiver2
+ collect()
+
+ # no disconnect signal is fired
+ assert len(sentinel) == 0
+
+ # and everything really is disconnected
+ sig.send('abc')
+ assert len(sentinel) == 0
+
+
+def test_signal_signals_weak_sender():
+ sentinel = Sentinel()
+ sig = blinker.Signal()
+
+ connected = sentinel.make_receiver('receiver_connected')
+ disconnected = sentinel.make_receiver('receiver_disconnected')
+ receiver1 = sentinel.make_receiver('receiver1')
+ receiver2 = sentinel.make_receiver('receiver2')
+
+ class Sender(object):
+ """A weakref-able object."""
+
+ sig.receiver_connected.connect(connected)
+ sig.receiver_disconnected.connect(disconnected)
+
+ sender1 = Sender()
+ sig.connect(receiver1, sender=sender1, weak=False)
+ # regular disconnect of weak-able sender works fine
+ sig.disconnect(receiver1, sender=sender1)
+
+ assert len(sentinel) == 2
+
+ del sentinel[:]
+ sender2 = Sender()
+ sig.connect(receiver2, sender=sender2, weak=False)
+
+ # force sender2 to go out of scope
+ del sender2
+ collect()
+
+ # no disconnect signal is fired
+ assert len(sentinel) == 1
+
+ # and everything really is disconnected
+ sig.send('abc')
+ assert len(sentinel) == 1
+
+
def test_meta_connect_failure():
def meta_received(sender, **kw):
raise TypeError('boom')