diff options
author | jason kirtland <jek@discorporate.us> | 2010-02-14 09:40:29 -0800 |
---|---|---|
committer | jason kirtland <jek@discorporate.us> | 2010-02-14 09:40:29 -0800 |
commit | c2d9461914c3624b49b596e3bd5d76dc32b406d1 (patch) | |
tree | f55897d79213aa56d9eec2444b701dac685d6f58 | |
download | blinker-c2d9461914c3624b49b596e3bd5d76dc32b406d1.tar.gz |
Initial import from flatland.util.signals
-rw-r--r-- | .hgignore | 6 | ||||
-rw-r--r-- | blinker/__init__.py | 17 | ||||
-rw-r--r-- | blinker/_saferef.py | 222 | ||||
-rw-r--r-- | blinker/_utilities.py | 79 | ||||
-rw-r--r-- | blinker/base.py | 220 | ||||
-rw-r--r-- | setup.py | 34 | ||||
-rw-r--r-- | tests/test_saferef.py | 117 | ||||
-rw-r--r-- | tests/test_signals.py | 235 | ||||
-rw-r--r-- | tests/test_utilities.py | 24 |
9 files changed, 954 insertions, 0 deletions
diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..9b7dcc2 --- /dev/null +++ b/.hgignore @@ -0,0 +1,6 @@ +syntax: glob + +.coverage +nosetests3 +bench +*.py? diff --git a/blinker/__init__.py b/blinker/__init__.py new file mode 100644 index 0000000..01ab251 --- /dev/null +++ b/blinker/__init__.py @@ -0,0 +1,17 @@ +from blinker.base import ( + ANY, + NamedSignal, + Namespace, + Signal, + receiver_connected, + signal, +) + +__all__ = [ + 'ANY', + 'NamedSignal', + 'Namespace', + 'Signal', + 'receiver_connected', + 'signal', + ] diff --git a/blinker/_saferef.py b/blinker/_saferef.py new file mode 100644 index 0000000..622ed99 --- /dev/null +++ b/blinker/_saferef.py @@ -0,0 +1,222 @@ +# extracted from Louie, http://pylouie.org/ +# updated for Python 3 +# +# Copyright (c) 2006 Patrick K. O'Brien, Mike C. Fletcher, +# Matthew R. Scott +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# +# * Neither the name of the <ORGANIZATION> nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +"""Refactored 'safe reference from dispatcher.py""" + +import weakref +import sys +import traceback + + +try: + callable +except NameError: + def callable(object): + return hasattr(object, '__call__') + + +def safe_ref(target, on_delete=None): + """Return a *safe* weak reference to a callable target. + + - ``target``: The object to be weakly referenced, if it's a bound + method reference, will create a BoundMethodWeakref, otherwise + creates a simple weakref. + + - ``on_delete``: If provided, will have a hard reference stored to + the callable to be called after the safe reference goes out of + scope with the reference object, (either a weakref or a + BoundMethodWeakref) as argument. + """ + if hasattr(target, 'im_self'): + if target.im_self is not None: + # Turn a bound method into a BoundMethodWeakref instance. + # Keep track of these instances for lookup by disconnect(). + assert hasattr(target, 'im_func'), ( + "safe_ref target %r has im_self, but no im_func, " + "don't know how to create reference" + % target + ) + reference = BoundMethodWeakref(target=target, on_delete=on_delete) + return reference + if callable(on_delete): + return weakref.ref(target, on_delete) + else: + return weakref.ref(target) + + +class BoundMethodWeakref(object): + """'Safe' and reusable weak references to instance methods. + + BoundMethodWeakref objects provide a mechanism for referencing a + bound method without requiring that the method object itself + (which is normally a transient object) is kept alive. Instead, + the BoundMethodWeakref object keeps weak references to both the + object and the function which together define the instance method. + + Attributes: + + - ``key``: The identity key for the reference, calculated by the + class's calculate_key method applied to the target instance method. + + - ``deletion_methods``: Sequence of callable objects taking single + argument, a reference to this object which will be called when + *either* the target object or target function is garbage + collected (i.e. when this object becomes invalid). These are + specified as the on_delete parameters of safe_ref calls. + + - ``weak_self``: Weak reference to the target object. + + - ``weak_func``: Weak reference to the target function. + + Class Attributes: + + - ``_all_instances``: Class attribute pointing to all live + BoundMethodWeakref objects indexed by the class's + calculate_key(target) method applied to the target objects. + This weak value dictionary is used to short-circuit creation so + that multiple references to the same (object, function) pair + produce the same BoundMethodWeakref instance. + """ + + _all_instances = weakref.WeakValueDictionary() + + def __new__(cls, target, on_delete=None, *arguments, **named): + """Create new instance or return current instance. + + Basically this method of construction allows us to + short-circuit creation of references to already- referenced + instance methods. The key corresponding to the target is + calculated, and if there is already an existing reference, + that is returned, with its deletion_methods attribute updated. + Otherwise the new instance is created and registered in the + table of already-referenced methods. + """ + key = cls.calculate_key(target) + current = cls._all_instances.get(key) + if current is not None: + current.deletion_methods.append(on_delete) + return current + else: + base = super(BoundMethodWeakref, cls).__new__(cls) + cls._all_instances[key] = base + base.__init__(target, on_delete, *arguments, **named) + return base + + def __init__(self, target, on_delete=None): + """Return a weak-reference-like instance for a bound method. + + - ``target``: The instance-method target for the weak reference, + must have im_self and im_func attributes and be + reconstructable via the following, which is true of built-in + instance methods:: + + target.im_func.__get__( target.im_self ) + + - ``on_delete``: Optional callback which will be called when + this weak reference ceases to be valid (i.e. either the + object or the function is garbage collected). Should take a + single argument, which will be passed a pointer to this + object. + """ + def remove(weak, self=self): + """Set self.isDead to True when method or instance is destroyed.""" + methods = self.deletion_methods[:] + del self.deletion_methods[:] + try: + del self.__class__._all_instances[self.key] + except KeyError: + pass + for function in methods: + try: + if callable(function): + function(self) + except Exception: + try: + traceback.print_exc() + except AttributeError: + e = sys.exc_info()[1] + print ('Exception during saferef %s ' + 'cleanup function %s: %s' % (self, function, e)) + self.deletion_methods = [on_delete] + self.key = self.calculate_key(target) + self.weak_self = weakref.ref(target.im_self, remove) + self.weak_func = weakref.ref(target.im_func, remove) + self.self_name = str(target.im_self) + self.func_name = str(target.im_func.__name__) + + def calculate_key(cls, target): + """Calculate the reference key for this reference. + + Currently this is a two-tuple of the id()'s of the target + object and the target function respectively. + """ + return (id(target.im_self), id(target.im_func)) + calculate_key = classmethod(calculate_key) + + def __str__(self): + """Give a friendly representation of the object.""" + return "%s(%s.%s)" % ( + self.__class__.__name__, + self.self_name, + self.func_name, + ) + + __repr__ = __str__ + + def __nonzero__(self): + """Whether we are still a valid reference.""" + return self() is not None + + def __cmp__(self, other): + """Compare with another reference.""" + if not isinstance(other, self.__class__): + return cmp(self.__class__, type(other)) + return cmp(self.key, other.key) + + def __call__(self): + """Return a strong reference to the bound method. + + If the target cannot be retrieved, then will return None, + otherwise returns a bound instance method for our object and + function. + + Note: You may call this method any number of times, as it does + not invalidate the reference. + """ + target = self.weak_self() + if target is not None: + function = self.weak_func() + if function is not None: + return function.__get__(target) + return None diff --git a/blinker/_utilities.py b/blinker/_utilities.py new file mode 100644 index 0000000..8ed1c3d --- /dev/null +++ b/blinker/_utilities.py @@ -0,0 +1,79 @@ +from weakref import ref + +from blinker._saferef import BoundMethodWeakref + + +try: + callable +except NameError: + def callable(object): + return hasattr(object, '__call__') + + +class _symbol(object): + + def __init__(self, name): + """Construct a new named symbol.""" + self.__name__ = self.name = name + + def __reduce__(self): + return symbol, (self.name,) + + def __repr__(self): + return self.name +_symbol.__name__ = 'symbol' + + +class symbol(object): + """A constant symbol. + + >>> symbol('foo') is symbol('foo') + True + >>> symbol('foo') + foo + + A slight refinement of the MAGICCOOKIE=object() pattern. The primary + advantage of symbol() is its repr(). They are also singletons. + + Repeated calls of symbol('name') will all return the same instance. + + """ + symbols = {} + + def __new__(cls, name): + try: + return cls.symbols[name] + except KeyError: + return cls.symbols.setdefault(name, _symbol(name)) + + +def hashable_identity(obj): + if hasattr(obj, 'im_func'): + return (id(obj.im_func), id(obj.im_self)) + else: + return id(obj) + + +WeakTypes = (ref, BoundMethodWeakref) + + +class annotatable_weakref(ref): + """A weakref.ref that supports custom instance attributes.""" + + +def reference(object, callback=None, **annotations): + """Return an annotated weak ref.""" + if callable(object): + weak = callable_reference(object, callback) + else: + weak = annotatable_weakref(object, callback) + for key, value in annotations.items(): + setattr(weak, key, value) + return weak + + +def callable_reference(object, callback=None): + """Return an annotated weak ref, supporting bound instance methods.""" + if hasattr(object, 'im_self') and object.im_self is not None: + return BoundMethodWeakref(target=object, on_delete=callback) + return annotatable_weakref(object, callback) diff --git a/blinker/base.py b/blinker/base.py new file mode 100644 index 0000000..310c1b6 --- /dev/null +++ b/blinker/base.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8; fill-column: 76 -*- +"""Signals and events. + +A small implementation of signals, inspired by a snippet of Django signal +API client code seen in a blog post. Signals are first-class objects and +each manages its own receivers and message emission. + +The :func:`signal` function provides singleton behavior for named signals. + +""" +from collections import defaultdict +from weakref import WeakValueDictionary + +from blinker._utilities import ( + WeakTypes, + hashable_identity, + reference, + symbol, + ) + + +ANY = symbol('ANY') +ANY_ID = 0 + + +class Signal(object): + """A generic notification emitter.""" + + #: A convenience for importers, allows Signal.ANY + ANY = ANY + + def __init__(self, doc=None): + if doc: + self.__doc__ = doc + self.receivers = {} + self._by_receiver = defaultdict(set) + self._by_sender = defaultdict(set) + self._weak_senders = {} + + def connect(self, receiver, sender=ANY, weak=True): + """Connect *receiver* to signal events send by *sender*. + + :param receiver: A callable. Will be invoked by :meth:`send`. Will + be invoked with `sender=` as a named argument and any \*\*kwargs + that were provided to a call to :meth:`send`. + + :param sender: Any object or :attr:`Signal.ANY`. Restricts + notifications to *receiver* to only those :meth:`send` emissions + sent by *sender*. If ``ANY``, the receiver will always be + notified. A *receiver* may be connected to multiple *sender* on + the same Signal. Defaults to ``ANY``. + + :param weak: If true, the Signal will hold a weakref to *receiver* + and automatically disconnect when *receiver* goes out of scope or + is garbage collected. Defaults to True. + + """ + receiver_id = hashable_identity(receiver) + if weak: + receiver_ref = reference(receiver, self._cleanup_receiver) + receiver_ref.receiver_id = receiver_id + else: + receiver_ref = receiver + sender_id = ANY_ID if sender is ANY else hashable_identity(sender) + + self.receivers.setdefault(receiver_id, receiver_ref) + self._by_sender[sender_id].add(receiver_id) + self._by_receiver[receiver_id].add(sender_id) + del receiver_ref + + if sender is not ANY and sender_id not in self._weak_senders: + # wire together a cleanup for weakref-able senders + try: + sender_ref = reference(sender, self._cleanup_sender) + sender_ref.sender_id = sender_id + except TypeError: + pass + else: + self._weak_senders.setdefault(sender_id, sender_ref) + del sender_ref + + # broadcast this connection. if receivers raise, disconnect. + if receiver_connected.receivers and self is not receiver_connected: + try: + receiver_connected.send(self, + receiver_arg=receiver, + sender_arg=sender, + weak_arg=weak) + except: + self.disconnect(receiver, sender) + raise + return receiver + + def send(self, *sender, **kwargs): + """Emit this signal on behalf of *sender*, passing on \*\*kwargs. + + Returns a list of 2-tuples, pairing receivers with their return + value. The ordering of receiver notification is undefined. + + :param \*sender: Any object or ``None``. If omitted, synonymous + with ``None``. Only accepts one positional argument. + + :param \*\*kwargs: Data to be sent to receivers. + + """ + # Using '*sender' rather than 'sender=None' allows 'sender' to be + # used as a keyword argument- i.e. it's an invisible name in the + # function signature. + if len(sender) == 0: + sender = None + elif len(sender) > 1: + raise TypeError('send() accepts only one positional argument, ' + '%s given' % len(sender)) + else: + sender = sender[0] + if not self.receivers: + return [] + else: + return [(receiver, receiver(sender, **kwargs)) + for receiver in self.receivers_for(sender)] + + def has_receivers_for(self, sender): + """True if there is probably a receiver for *sender*. + + Performs an optimistic check for receivers only. Does not guarantee + that all weakly referenced receivers are still alive. See + :meth:`receivers_for` for a stronger search. + + """ + if not self.receivers: + return False + if self._by_sender[ANY_ID]: + return True + if sender is ANY: + return False + return hashable_identity(sender) in self._by_sender + + def receivers_for(self, sender): + """Iterate all live receivers listening for *sender*.""" + # TODO: test receivers_for(ANY) + if self.receivers: + sender_id = hashable_identity(sender) + if sender_id in self._by_sender: + ids = (self._by_sender[ANY_ID] | + self._by_sender[sender_id]) + else: + ids = self._by_sender[ANY_ID].copy() + for receiver_id in ids: + receiver = self.receivers.get(receiver_id) + if receiver is None: + continue + if isinstance(receiver, WeakTypes): + strong = receiver() + if strong is None: + self._disconnect(receiver_id, ANY_ID) + continue + receiver = strong + yield receiver + + def disconnect(self, receiver, sender=ANY): + """Disconnect *receiver* from this signal's events.""" + sender_id = ANY_ID if sender is ANY else hashable_identity(sender) + receiver_id = hashable_identity(receiver) + self._disconnect(receiver_id, sender_id) + + def _disconnect(self, receiver_id, sender_id): + if sender_id == ANY_ID: + if self._by_receiver.pop(receiver_id, False): + for bucket in self._by_sender.values(): + bucket.discard(receiver_id) + self.receivers.pop(receiver_id, None) + else: + self._by_sender[sender_id].discard(receiver_id) + + def _cleanup_receiver(self, receiver_ref): + """Disconnect a receiver from all senders.""" + self._disconnect(receiver_ref.receiver_id, ANY_ID) + + def _cleanup_sender(self, sender_ref): + """Disconnect all receivers from a sender.""" + sender_id = sender_ref.sender_id + assert sender_id != ANY_ID + self._weak_senders.pop(sender_id, None) + for receiver_id in self._by_sender.pop(sender_id, ()): + self._by_receiver[receiver_id].discard(sender_id) + + def _clear_state(self): + """Throw away all signal state. Useful for unit tests.""" + self._weak_senders.clear() + self.receivers.clear() + self._by_sender.clear() + self._by_receiver.clear() + + +receiver_connected = Signal() + + +class NamedSignal(Signal): + """A named generic notification emitter.""" + + def __init__(self, name, doc=None): + Signal.__init__(self, doc) + self.name = name + + def __repr__(self): + base = Signal.__repr__(self) + return "%s; %r>" % (base[:-1], self.name) + + +class Namespace(WeakValueDictionary): + + def signal(self, name, doc=None): + """Return the :class:`NamedSignal` *name*, creating it if required.""" + try: + return self[name] + except KeyError: + return self.setdefault(name, NamedSignal(name, doc)) + + +signal = Namespace().signal diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..a5b95f0 --- /dev/null +++ b/setup.py @@ -0,0 +1,34 @@ +""" +blinker +~~~~~~~ + + +""" +from distutils.tools import setup + + +setup(name="blinker", + version="0.8", + packages=['blinker'], + author='Jason Kirtland', + author_email='jek@discorporate.us', + description='fast and simple object-to-object and broadcast signalling', + keywords='signal emit events broadcast', + long_description=__doc__, + license='MIT License', + url='http://discorporate.us/jek/projects/blinker/', + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.4', + 'Programming Language :: Python :: 2.5', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + # 'Programming Language :: Python :: 3.1', + 'Topic :: Software Development :: Libraries' + 'Topic :: Utilities', + ], +) diff --git a/tests/test_saferef.py b/tests/test_saferef.py new file mode 100644 index 0000000..3886a9e --- /dev/null +++ b/tests/test_saferef.py @@ -0,0 +1,117 @@ +# extracted from Louie, http://pylouie.org/ +# updated for Python 3 +# +# Copyright (c) 2006 Patrick K. O'Brien, Mike C. Fletcher, +# Matthew R. Scott +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# +# * Neither the name of the <ORGANIZATION> nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +import unittest + +from blinker._saferef import safe_ref + + +class _Sample1(object): + def x(self): + pass + + +def _sample2(obj): + pass + + +class _Sample3(object): + def __call__(self, obj): + pass + + +class TestSaferef(unittest.TestCase): + + # XXX: The original tests had a test for closure, and it had an + # off-by-one problem, perhaps due to scope issues. It has been + # removed from this test suite. + + def setUp(self): + ts = [] + ss = [] + for x in range(100): + t = _Sample1() + ts.append(t) + s = safe_ref(t.x, self._closure) + ss.append(s) + ts.append(_sample2) + ss.append(safe_ref(_sample2, self._closure)) + for x in range(30): + t = _Sample3() + ts.append(t) + s = safe_ref(t, self._closure) + ss.append(s) + self.ts = ts + self.ss = ss + self.closure_count = 0 + + def tearDown(self): + if hasattr(self, 'ts'): + del self.ts + if hasattr(self, 'ss'): + del self.ss + + def test_In(self): + """Test the `in` operator for safe references (cmp)""" + for t in self.ts[:50]: + assert safe_ref(t.x) in self.ss + + def test_Valid(self): + """Test that the references are valid (return instance methods)""" + for s in self.ss: + assert s() + + def test_ShortCircuit(self): + """Test that creation short-circuits to reuse existing references""" + sd = {} + for s in self.ss: + sd[s] = 1 + for t in self.ts: + if hasattr(t, 'x'): + assert sd.has_key(safe_ref(t.x)) + else: + assert sd.has_key(safe_ref(t)) + + def test_Representation(self): + """Test that the reference object's representation works + + XXX Doesn't currently check the results, just that no error + is raised + """ + repr(self.ss[-1]) + + def _closure(self, ref): + """Dumb utility mechanism to increment deletion counter""" + self.closure_count += 1 + diff --git a/tests/test_signals.py b/tests/test_signals.py new file mode 100644 index 0000000..9715293 --- /dev/null +++ b/tests/test_signals.py @@ -0,0 +1,235 @@ +import blinker +from nose.tools import assert_raises + + +def test_meta_connect(): + sentinel = [] + def meta_received(sender, **kw): + sentinel.append(dict(kw, sender=sender)) + + assert not blinker.receiver_connected.receivers + blinker.receiver_connected.connect(meta_received) + assert not sentinel + + def receiver(sender, **kw): + pass + sig = blinker.Signal() + sig.connect(receiver) + + assert sentinel == [dict(sender=sig, + receiver_arg=receiver, + sender_arg=blinker.ANY, + weak_arg=True)] + + blinker.receiver_connected._clear_state() + + +def test_meta_connect_failure(): + def meta_received(sender, **kw): + raise TypeError('boom') + + assert not blinker.receiver_connected.receivers + blinker.receiver_connected.connect(meta_received) + + def receiver(sender, **kw): + pass + sig = blinker.Signal() + + assert_raises(TypeError, sig.connect, receiver) + assert not sig.receivers + assert not sig._by_receiver + assert sig._by_sender == {blinker.base.ANY_ID: set()} + + blinker.receiver_connected._clear_state() + + +def test_singletons(): + ns = blinker.Namespace() + assert not ns + s1 = ns.signal('abc') + assert s1 is ns.signal('abc') + assert s1 is not ns.signal('def') + assert 'abc' in ns + # weak by default, already out of scope + assert 'def' not in ns + del s1 + assert 'abc' not in ns + + +def test_weak_receiver(): + sentinel = [] + def received(**kw): + sentinel.append(kw) + + sig = blinker.Signal() + sig.connect(received, weak=True) + del received + + assert not sentinel + sig.send() + assert not sentinel + assert not sig.receivers + values_are_empty_sets_(sig._by_receiver) + values_are_empty_sets_(sig._by_sender) + + +def test_strong_receiver(): + sentinel = [] + def received(sender): + sentinel.append(sender) + fn_id = id(received) + + sig = blinker.Signal() + sig.connect(received, weak=False) + del received + + assert not sentinel + sig.send() + assert sentinel + assert [id(fn) for fn in sig.receivers.values()] == [fn_id] + + +def test_instancemethod_receiver(): + sentinel = [] + + class Receiver(object): + def __init__(self, bucket): + self.bucket = bucket + def received(self, sender): + self.bucket.append(sender) + + receiver = Receiver(sentinel) + + sig = blinker.Signal() + sig.connect(receiver.received) + + assert not sentinel + sig.send() + assert sentinel + del receiver + sig.send() + assert len(sentinel) == 1 + + +def test_filtered_receiver(): + sentinel = [] + def received(sender): + sentinel.append(sender) + + sig = blinker.Signal() + + sig.connect(received, 123) + + assert not sentinel + sig.send() + assert not sentinel + sig.send(123) + assert sentinel == [123] + sig.send() + assert sentinel == [123] + + sig.disconnect(received, 123) + sig.send(123) + assert sentinel == [123] + + sig.connect(received, 123) + sig.send(123) + assert sentinel == [123, 123] + + sig.disconnect(received) + sig.send(123) + assert sentinel == [123, 123] + + +def test_filtered_receiver_weakref(): + sentinel = [] + def received(sender): + sentinel.append(sender) + + class Object(object): + pass + obj = Object() + + sig = blinker.Signal() + sig.connect(received, obj) + + assert not sentinel + sig.send(obj) + assert sentinel == [obj] + del sentinel[:] + del obj + + # general index isn't cleaned up + assert sig.receivers + # but receiver/sender pairs are + values_are_empty_sets_(sig._by_receiver) + values_are_empty_sets_(sig._by_sender) + + +def test_no_double_send(): + sentinel = [] + def received(sender): + sentinel.append(sender) + + sig = blinker.Signal() + + sig.connect(received, 123) + sig.connect(received) + + assert not sentinel + sig.send() + assert sentinel == [None] + sig.send(123) + assert sentinel == [None, 123] + sig.send() + assert sentinel == [None, 123, None] + + +def test_has_receivers(): + received = lambda sender: None + + sig = blinker.Signal() + assert not sig.has_receivers_for(None) + assert not sig.has_receivers_for(blinker.ANY) + + sig.connect(received, 'xyz') + assert not sig.has_receivers_for(None) + assert not sig.has_receivers_for(blinker.ANY) + assert sig.has_receivers_for('xyz') + + class Object(object): + pass + o = Object() + + sig.connect(received, o) + assert sig.has_receivers_for(o) + + del received + assert not sig.has_receivers_for('xyz') + assert list(sig.receivers_for('xyz')) == [] + assert list(sig.receivers_for(o)) == [] + + sig.connect(lambda sender: None, weak=False) + assert sig.has_receivers_for('xyz') + assert sig.has_receivers_for(o) + assert sig.has_receivers_for(None) + assert sig.has_receivers_for(blinker.ANY) + assert sig.has_receivers_for('xyz') + + +def test_instance_doc(): + sig = blinker.Signal(doc='x') + assert sig.__doc__ == 'x' + + sig = blinker.Signal('x') + assert sig.__doc__ == 'x' + + +def test_named_blinker(): + sig = blinker.NamedSignal('squiznart') + assert 'squiznart' in repr(sig) + + +def values_are_empty_sets_(dictionary): + for val in dictionary.values(): + assert val == set() diff --git a/tests/test_utilities.py b/tests/test_utilities.py new file mode 100644 index 0000000..4f802f4 --- /dev/null +++ b/tests/test_utilities.py @@ -0,0 +1,24 @@ +import pickle + +from blinker._utilities import symbol + + +def test_symbols(): + foo = symbol('foo') + assert foo.name == 'foo' + assert foo is symbol('foo') + + bar = symbol('bar') + assert foo is not bar + assert foo != bar + assert not foo == bar + + assert repr(foo) == 'foo' + + +def test_pickled_symbols(): + foo = symbol('foo') + + for protocol in 0, 1, 2: + roundtrip = pickle.loads(pickle.dumps(foo)) + assert roundtrip is foo |