diff --git a/README.md b/README.md index d2954ab16e..d750f17c44 100644 --- a/README.md +++ b/README.md @@ -305,6 +305,7 @@ If using multiple usernames format like this: * DavidK1m * budi-khoirudin * riberod07 + * namlehong ------- ## Credits diff --git a/dispatcher/__init__.py b/dispatcher/__init__.py new file mode 100644 index 0000000000..9e7200a899 --- /dev/null +++ b/dispatcher/__init__.py @@ -0,0 +1,305 @@ +import sys +import threading +import warnings +import weakref + +import six +# from six.moves import range + +if six.PY2: + from .weakref_backports import WeakMethod +else: + from weakref import WeakMethod + + +def _make_id(target): + if hasattr(target, '__func__'): + return (id(target.__self__), id(target.__func__)) + return id(target) +NONE_ID = _make_id(None) + +# A marker for caching +NO_RECEIVERS = object() + + +class Signal(object): + """ + Base class for all signals + + Internal attributes: + + receivers + { receiverkey (id) : weakref(receiver) } + """ + def __init__(self, providing_args=None, use_caching=False): + """ + Create a new signal. + + providing_args + A list of the arguments this signal can pass along in a send() call. + """ + self.receivers = [] + if providing_args is None: + providing_args = [] + self.providing_args = set(providing_args) + self.lock = threading.Lock() + self.use_caching = use_caching + # For convenience we create empty caches even if they are not used. + # A note about caching: if use_caching is defined, then for each + # distinct sender we cache the receivers that sender has in + # 'sender_receivers_cache'. The cache is cleaned when .connect() or + # .disconnect() is called and populated on send(). + self.sender_receivers_cache = weakref.WeakKeyDictionary() if use_caching else {} + self._dead_receivers = False + + def connect(self, receiver, sender=None, weak=True, dispatch_uid=None): + """ + Connect receiver to sender for signal. + + Arguments: + + receiver + A function or an instance method which is to receive signals. + Receivers must be hashable objects. + + If weak is True, then receiver must be weak referenceable. + + Receivers must be able to accept keyword arguments. + + If a receiver is connected with a dispatch_uid argument, it + will not be added if another receiver was already connected + with that dispatch_uid. + + sender + The sender to which the receiver should respond. Must either be + of type Signal, or None to receive events from any sender. + + weak + Whether to use weak references to the receiver. By default, the + module will attempt to use weak references to the receiver + objects. If this parameter is false, then strong references will + be used. + + dispatch_uid + An identifier used to uniquely identify a particular instance of + a receiver. This will usually be a string, though it may be + anything hashable. + """ + + if dispatch_uid: + lookup_key = (dispatch_uid, _make_id(sender)) + else: + lookup_key = (_make_id(receiver), _make_id(sender)) + + if weak: + ref = weakref.ref + receiver_object = receiver + # Check for bound methods + if hasattr(receiver, '__self__') and hasattr(receiver, '__func__'): + ref = WeakMethod + receiver_object = receiver.__self__ + if six.PY3: + receiver = ref(receiver) + weakref.finalize(receiver_object, self._remove_receiver) + else: + receiver = ref(receiver, self._remove_receiver) + + with self.lock: + self._clear_dead_receivers() + for r_key, _ in self.receivers: + if r_key == lookup_key: + break + else: + self.receivers.append((lookup_key, receiver)) + self.sender_receivers_cache.clear() + + def disconnect(self, receiver=None, sender=None, weak=None, dispatch_uid=None): + """ + Disconnect receiver from sender for signal. + + If weak references are used, disconnect need not be called. The receiver + will be remove from dispatch automatically. + + Arguments: + + receiver + The registered receiver to disconnect. May be none if + dispatch_uid is specified. + + sender + The registered sender to disconnect + + dispatch_uid + the unique identifier of the receiver to disconnect + """ + if weak is not None: + warnings.warn("Passing `weak` to disconnect has no effect.", stacklevel=2) + if dispatch_uid: + lookup_key = (dispatch_uid, _make_id(sender)) + else: + lookup_key = (_make_id(receiver), _make_id(sender)) + + disconnected = False + with self.lock: + self._clear_dead_receivers() + for index in range(len(self.receivers)): + (r_key, _) = self.receivers[index] + if r_key == lookup_key: + disconnected = True + del self.receivers[index] + break + self.sender_receivers_cache.clear() + return disconnected + + def has_listeners(self, sender=None): + return bool(self._live_receivers(sender)) + + def send(self, sender, **named): + """ + Send signal from sender to all connected receivers. + + If any receiver raises an error, the error propagates back through send, + terminating the dispatch loop. So it's possible that all receivers + won't be called if an error is raised. + + Arguments: + + sender + The sender of the signal. Either a specific object or None. + + named + Named arguments which will be passed to receivers. + + Returns a list of tuple pairs [(receiver, response), ... ]. + """ + responses = [] + if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS: + return responses + + for receiver in self._live_receivers(sender): + response = receiver(signal=self, sender=sender, **named) + responses.append((receiver, response)) + return responses + + def send_robust(self, sender, **named): + """ + Send signal from sender to all connected receivers catching errors. + + Arguments: + + sender + The sender of the signal. Can be any python object (normally one + registered with a connect if you actually want something to + occur). + + named + Named arguments which will be passed to receivers. These + arguments must be a subset of the argument names defined in + providing_args. + + Return a list of tuple pairs [(receiver, response), ... ]. May raise + DispatcherKeyError. + + If any receiver raises an error (specifically any subclass of + Exception), the error instance is returned as the result for that + receiver. The traceback is always attached to the error at + ``__traceback__``. + """ + responses = [] + if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS: + return responses + + # Call each receiver with whatever arguments it can accept. + # Return a list of tuple pairs [(receiver, response), ... ]. + for receiver in self._live_receivers(sender): + try: + response = receiver(signal=self, sender=sender, **named) + except Exception as err: + if not hasattr(err, '__traceback__'): + err.__traceback__ = sys.exc_info()[2] + responses.append((receiver, err)) + else: + responses.append((receiver, response)) + return responses + + def _clear_dead_receivers(self): + # Note: caller is assumed to hold self.lock. + if self._dead_receivers: + self._dead_receivers = False + new_receivers = [] + for r in self.receivers: + if isinstance(r[1], weakref.ReferenceType) and r[1]() is None: + continue + new_receivers.append(r) + self.receivers = new_receivers + + def _live_receivers(self, sender): + """ + Filter sequence of receivers to get resolved, live receivers. + + This checks for weak references and resolves them, then returning only + live receivers. + """ + receivers = None + if self.use_caching and not self._dead_receivers: + receivers = self.sender_receivers_cache.get(sender) + # We could end up here with NO_RECEIVERS even if we do check this case in + # .send() prior to calling _live_receivers() due to concurrent .send() call. + if receivers is NO_RECEIVERS: + return [] + if receivers is None: + with self.lock: + self._clear_dead_receivers() + senderkey = _make_id(sender) + receivers = [] + for (receiverkey, r_senderkey), receiver in self.receivers: + if r_senderkey == NONE_ID or r_senderkey == senderkey: + receivers.append(receiver) + if self.use_caching: + if not receivers: + self.sender_receivers_cache[sender] = NO_RECEIVERS + else: + # Note, we must cache the weakref versions. + self.sender_receivers_cache[sender] = receivers + non_weak_receivers = [] + for receiver in receivers: + if isinstance(receiver, weakref.ReferenceType): + # Dereference the weak reference. + receiver = receiver() + if receiver is not None: + non_weak_receivers.append(receiver) + else: + non_weak_receivers.append(receiver) + return non_weak_receivers + + def _remove_receiver(self, receiver=None): + # Mark that the self.receivers list has dead weakrefs. If so, we will + # clean those up in connect, disconnect and _live_receivers while + # holding self.lock. Note that doing the cleanup here isn't a good + # idea, _remove_receiver() will be called as side effect of garbage + # collection, and so the call can happen while we are already holding + # self.lock. + self._dead_receivers = True + + +def receiver(signal, **kwargs): + """ + A decorator for connecting receivers to signals. Used by passing in the + signal (or list of signals) and keyword arguments to connect:: + + @receiver(post_save, sender=MyModel) + def signal_receiver(sender, **kwargs): + ... + + @receiver([post_save, post_delete], sender=MyModel) + def signals_receiver(sender, **kwargs): + ... + """ + def _decorator(func): + if isinstance(signal, (list, tuple)): + for s in signal: + s.connect(func, **kwargs) + else: + signal.connect(func, **kwargs) + return func + return _decorator diff --git a/dispatcher/weakref_backports.py b/dispatcher/weakref_backports.py new file mode 100644 index 0000000000..1f996f2dbf --- /dev/null +++ b/dispatcher/weakref_backports.py @@ -0,0 +1,55 @@ +from weakref import ref + + +class WeakMethod(ref): + """ + A custom `weakref.ref` subclass which simulates a weak reference to + a bound method, working around the lifetime problem of bound methods. + """ + + __slots__ = "_func_ref", "_meth_type", "_alive", "__weakref__" + + def __new__(cls, meth, callback=None): + try: + obj = meth.__self__ + func = meth.__func__ + except AttributeError: + raise TypeError("argument should be a bound method, not {}" + .format(type(meth))) + def _cb(arg): + # The self-weakref trick is needed to avoid creating a reference + # cycle. + self = self_wr() + if self._alive: + self._alive = False + if callback is not None: + callback(self) + self = ref.__new__(cls, obj, _cb) + self._func_ref = ref(func, _cb) + self._meth_type = type(meth) + self._alive = True + self_wr = ref(self) + return self + + def __call__(self): + obj = super(WeakMethod, self).__call__() + func = self._func_ref() + if obj is None or func is None: + return None + return self._meth_type(func, obj) + + def __eq__(self, other): + if isinstance(other, WeakMethod): + if not self._alive or not other._alive: + return self is other + return ref.__eq__(self, other) and self._func_ref == other._func_ref + return False + + def __ne__(self, other): + if isinstance(other, WeakMethod): + if not self._alive or not other._alive: + return self is not other + return ref.__ne__(self, other) or self._func_ref != other._func_ref + return True + + __hash__ = ref.__hash__ diff --git a/test_receiver.py b/test_receiver.py new file mode 100644 index 0000000000..0cfae26b80 --- /dev/null +++ b/test_receiver.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from dispatcher import Signal, receiver + +sign_in_signal = Signal(providing_args=['player', 'instance']) + + +class MyBot(object): + def __init__(self, player=None): + if not player: + player = {} + self.player = player + + def login(self): + sign_in_signal.send(sender=self.__class__, instance=self, player=self.player) + + +def sign_in_handler(sender, instance, player, **kwargs): + print 'sign_in_handler' + print 'sender', sender + print 'instance', instance + print 'actor', player + + +@receiver([sign_in_signal], sender=MyBot) +def another_sign_in_hander(sender, **kwargs): + print 'another_sign_in_hander', sender, kwargs + + +sign_in_signal.connect(sign_in_handler, sender=MyBot) + +if __name__ == '__main__': + bot = MyBot(player={'name': 'justAName'}) + bot.login()