signals.py 4.26 KB
# -*- coding: UTF-8 -*-
"""Module that provides a simple signals library.

The signals pattern is very similar to the listener/observer pattern.

"""
from inspect import ismethod
from threading import Lock

from py4j.compat import range


def make_id(func):
    if ismethod(func):
        return (id(func.__self__), id(func.__func__))
    return id(func)

NONE_ID = make_id(None)


class Signal(object):
    """Basic signal class that can register receivers (listeners) and dispatch
    events to these receivers.

    As opposed to many signals libraries, receivers are not stored as weak
    references, so it is us to the client application to unregister them.

    Greatly inspired from Django Signals:
    https://github.com/django/django/blob/master/django/dispatch/dispatcher.py
    """

    def __init__(self):
        self.lock = Lock()
        # Someday, we may implement caching, but in practice, we expect the
        # number of receivers to be very small.
        self.receivers = []

    def connect(self, receiver, sender=None, unique_id=None):
        """Registers a receiver for this signal.

        The receiver must be a callable (e.g., function or instance method)
        that accepts named arguments (i.e., ``**kwargs``).

        In case that the connect method might be called multiple time, it is
        best to provide the receiver with a unique id to make sure that the
        receiver is not registered more than once.

        :param receiver: The callable that will receive the signal.
        :param sender: The sender to which the receiver will respond to. If
            None, signals from any sender are sent to this receiver
        :param unique_id: The unique id of the callable to make sure it is not
            registered more than once. Optional.
        """
        full_id = self._get_id(receiver, unique_id, sender)

        with self.lock:
            for receiver_id, _ in self.receivers:
                if receiver_id == full_id:
                    break
            else:
                self.receivers.append((full_id, receiver))

    def disconnect(self, receiver, sender=None, unique_id=None):
        """Unregisters a receiver for this signal.

        :param receiver: The callable that was registered to receive the
            signal.
        :param unique_id: The unique id of the callable if it was provided.
            Optional.
        :return: True if the receiver was found and disconnected. False
            otherwise.
        :rtype: bool
        """
        full_id = self._get_id(receiver, unique_id, sender)
        disconnected = False

        with self.lock:
            for index in range(len(self.receivers)):
                temp_id = self.receivers[index][0]
                if temp_id == full_id:
                    del self.receivers[index]
                    disconnected = True
                    break

        return disconnected

    def send(self, sender, **params):
        """Sends the signal to all connected receivers.

        If a receiver raises an error, the error is propagated back and
        interrupts the sending processing. It is thus possible that not all
        receivers will receive the signal.

        :param: named parameters to send to the receivers.
        :param: the sender of the signal. Optional.
        :return: List of (receiver, response) from receivers.
        :rtype: list
        """
        responses = []
        for receiver in self._get_receivers(sender):
            response = receiver(signal=self, sender=sender, **params)
            responses.append((receiver, response))
        return responses

    def _get_receivers(self, sender):
        """Internal method that may in the future resolve weak references or
        perform other work such as identifying dead receivers.
        """
        sender_id = make_id(sender)
        receivers = []
        with self.lock:
            for ((_, rsender_id), receiver) in self.receivers:
                if rsender_id == NONE_ID or rsender_id == sender_id:
                    receivers.append(receiver)
        return receivers

    def _get_id(self, receiver, unique_id, sender):
        sender_id = make_id(sender)
        if unique_id:
            full_id = (unique_id, sender_id)
        else:
            full_id = (make_id(receiver), sender_id)
        return full_id