diff options
author | jason kirtland <jek@discorporate.us> | 2011-03-14 17:12:59 -0400 |
---|---|---|
committer | jason kirtland <jek@discorporate.us> | 2011-03-14 17:12:59 -0400 |
commit | d24501d9f717e930f059b38e3043790edaf35f0c (patch) | |
tree | 1342993ec69016644f50026c9d0060ef4cdc05f4 | |
parent | 6096207fa513d8bd7518e9d29f47ea126ea6ed09 (diff) | |
download | blinker-d24501d9f717e930f059b38e3043790edaf35f0c.tar.gz |
Added Signal.receiver_connected and .receiver_disconnected per-Signal signals.
Deprecates the global 'receiver_connected' signal.
-rw-r--r-- | CHANGES | 11 | ||||
-rw-r--r-- | blinker/_utilities.py | 15 | ||||
-rw-r--r-- | blinker/base.py | 60 | ||||
-rw-r--r-- | tests/test_signals.py | 139 |
4 files changed, 225 insertions, 0 deletions
@@ -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') |