496 lines
17 KiB
Python
496 lines
17 KiB
Python
"""Multiple-producer-multiple-consumer signal-dispatching
|
|
|
|
dispatcher is the core of the PyDispatcher system,
|
|
providing the primary API and the core logic for the
|
|
system.
|
|
|
|
Module attributes of note:
|
|
|
|
Any -- Singleton used to signal either "Any Sender" or
|
|
"Any Signal". See documentation of the _Any class.
|
|
Anonymous -- Singleton used to signal "Anonymous Sender"
|
|
See documentation of the _Anonymous class.
|
|
|
|
Internal attributes:
|
|
WEAKREF_TYPES -- tuple of types/classes which represent
|
|
weak references to receivers, and thus must be de-
|
|
referenced on retrieval to retrieve the callable
|
|
object
|
|
connections -- { senderkey (id) : { signal : [receivers...]}}
|
|
senders -- { senderkey (id) : weakref(sender) }
|
|
used for cleaning up sender references on sender
|
|
deletion
|
|
sendersBack -- { receiverkey (id) : [senderkey (id)...] }
|
|
used for cleaning up receiver references on receiver
|
|
deletion, (considerably speeds up the cleanup process
|
|
vs. the original code.)
|
|
"""
|
|
import weakref
|
|
from django.dispatch import saferef, robustapply, errors
|
|
|
|
__author__ = "Patrick K. O'Brien <pobrien@orbtech.com>"
|
|
__cvsid__ = "$Id: dispatcher.py,v 1.9 2005/09/17 04:55:57 mcfletch Exp $"
|
|
__version__ = "$Revision: 1.9 $"[11:-2]
|
|
|
|
|
|
class _Parameter:
|
|
"""Used to represent default parameter values."""
|
|
def __repr__(self):
|
|
return self.__class__.__name__
|
|
|
|
class _Any(_Parameter):
|
|
"""Singleton used to signal either "Any Sender" or "Any Signal"
|
|
|
|
The Any object can be used with connect, disconnect,
|
|
send, or sendExact to signal that the parameter given
|
|
Any should react to all senders/signals, not just
|
|
a particular sender/signal.
|
|
"""
|
|
Any = _Any()
|
|
|
|
class _Anonymous(_Parameter):
|
|
"""Singleton used to signal "Anonymous Sender"
|
|
|
|
The Anonymous object is used to signal that the sender
|
|
of a message is not specified (as distinct from being
|
|
"any sender"). Registering callbacks for Anonymous
|
|
will only receive messages sent without senders. Sending
|
|
with anonymous will only send messages to those receivers
|
|
registered for Any or Anonymous.
|
|
|
|
Note:
|
|
The default sender for connect is Any, while the
|
|
default sender for send is Anonymous. This has
|
|
the effect that if you do not specify any senders
|
|
in either function then all messages are routed
|
|
as though there was a single sender (Anonymous)
|
|
being used everywhere.
|
|
"""
|
|
Anonymous = _Anonymous()
|
|
|
|
WEAKREF_TYPES = (weakref.ReferenceType, saferef.BoundMethodWeakref)
|
|
|
|
connections = {}
|
|
senders = {}
|
|
sendersBack = {}
|
|
|
|
|
|
def connect(receiver, signal=Any, sender=Any, weak=True):
|
|
"""Connect receiver to sender for signal
|
|
|
|
receiver -- a callable Python object which is to receive
|
|
messages/signals/events. Receivers must be hashable
|
|
objects.
|
|
|
|
if weak is True, then receiver must be weak-referencable
|
|
(more precisely saferef.safeRef() must be able to create
|
|
a reference to the receiver).
|
|
|
|
Receivers are fairly flexible in their specification,
|
|
as the machinery in the robustApply module takes care
|
|
of most of the details regarding figuring out appropriate
|
|
subsets of the sent arguments to apply to a given
|
|
receiver.
|
|
|
|
Note:
|
|
if receiver is itself a weak reference (a callable),
|
|
it will be de-referenced by the system's machinery,
|
|
so *generally* weak references are not suitable as
|
|
receivers, though some use might be found for the
|
|
facility whereby a higher-level library passes in
|
|
pre-weakrefed receiver references.
|
|
|
|
signal -- the signal to which the receiver should respond
|
|
|
|
if Any, receiver will receive any signal from the
|
|
indicated sender (which might also be Any, but is not
|
|
necessarily Any).
|
|
|
|
Otherwise must be a hashable Python object other than
|
|
None (DispatcherError raised on None).
|
|
|
|
sender -- the sender to which the receiver should respond
|
|
|
|
if Any, receiver will receive the indicated signals
|
|
from any sender.
|
|
|
|
if Anonymous, receiver will only receive indicated
|
|
signals from send/sendExact which do not specify a
|
|
sender, or specify Anonymous explicitly as the sender.
|
|
|
|
Otherwise can be any python object.
|
|
|
|
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.
|
|
|
|
returns None, may raise DispatcherTypeError
|
|
"""
|
|
if signal is None:
|
|
raise errors.DispatcherTypeError(
|
|
'Signal cannot be None (receiver=%r sender=%r)' % (receiver, sender)
|
|
)
|
|
if weak:
|
|
receiver = saferef.safeRef(receiver, onDelete=_removeReceiver)
|
|
senderkey = id(sender)
|
|
|
|
signals = connections.setdefault(senderkey, {})
|
|
|
|
# Keep track of senders for cleanup.
|
|
# Is Anonymous something we want to clean up?
|
|
if sender not in (None, Anonymous, Any):
|
|
def remove(object, senderkey=senderkey):
|
|
_removeSender(senderkey=senderkey)
|
|
# Skip objects that can not be weakly referenced, which means
|
|
# they won't be automatically cleaned up, but that's too bad.
|
|
try:
|
|
weakSender = weakref.ref(sender, remove)
|
|
senders[senderkey] = weakSender
|
|
except:
|
|
pass
|
|
|
|
receiverID = id(receiver)
|
|
# get current set, remove any current references to
|
|
# this receiver in the set, including back-references
|
|
if signals.has_key(signal):
|
|
receivers = signals[signal]
|
|
_removeOldBackRefs(senderkey, signal, receiver, receivers)
|
|
else:
|
|
receivers = signals[signal] = []
|
|
try:
|
|
current = sendersBack.get(receiverID)
|
|
if current is None:
|
|
sendersBack[ receiverID ] = current = []
|
|
if senderkey not in current:
|
|
current.append(senderkey)
|
|
except:
|
|
pass
|
|
|
|
receivers.append(receiver)
|
|
|
|
|
|
|
|
def disconnect(receiver, signal=Any, sender=Any, weak=True):
|
|
"""Disconnect receiver from sender for signal
|
|
|
|
receiver -- the registered receiver to disconnect
|
|
signal -- the registered signal to disconnect
|
|
sender -- the registered sender to disconnect
|
|
weak -- the weakref state to disconnect
|
|
|
|
disconnect reverses the process of connect,
|
|
the semantics for the individual elements are
|
|
logically equivalent to a tuple of
|
|
(receiver, signal, sender, weak) used as a key
|
|
to be deleted from the internal routing tables.
|
|
(The actual process is slightly more complex
|
|
but the semantics are basically the same).
|
|
|
|
Note:
|
|
Using disconnect is not required to cleanup
|
|
routing when an object is deleted, the framework
|
|
will remove routes for deleted objects
|
|
automatically. It's only necessary to disconnect
|
|
if you want to stop routing to a live object.
|
|
|
|
returns None, may raise DispatcherTypeError or
|
|
DispatcherKeyError
|
|
"""
|
|
if signal is None:
|
|
raise errors.DispatcherTypeError(
|
|
'Signal cannot be None (receiver=%r sender=%r)' % (receiver, sender)
|
|
)
|
|
if weak: receiver = saferef.safeRef(receiver)
|
|
senderkey = id(sender)
|
|
try:
|
|
signals = connections[senderkey]
|
|
receivers = signals[signal]
|
|
except KeyError:
|
|
raise errors.DispatcherKeyError(
|
|
"""No receivers found for signal %r from sender %r""" %(
|
|
signal,
|
|
sender
|
|
)
|
|
)
|
|
try:
|
|
# also removes from receivers
|
|
_removeOldBackRefs(senderkey, signal, receiver, receivers)
|
|
except ValueError:
|
|
raise errors.DispatcherKeyError(
|
|
"""No connection to receiver %s for signal %s from sender %s""" %(
|
|
receiver,
|
|
signal,
|
|
sender
|
|
)
|
|
)
|
|
_cleanupConnections(senderkey, signal)
|
|
|
|
def getReceivers(sender=Any, signal=Any):
|
|
"""Get list of receivers from global tables
|
|
|
|
This utility function allows you to retrieve the
|
|
raw list of receivers from the connections table
|
|
for the given sender and signal pair.
|
|
|
|
Note:
|
|
there is no guarantee that this is the actual list
|
|
stored in the connections table, so the value
|
|
should be treated as a simple iterable/truth value
|
|
rather than, for instance a list to which you
|
|
might append new records.
|
|
|
|
Normally you would use liveReceivers(getReceivers(...))
|
|
to retrieve the actual receiver objects as an iterable
|
|
object.
|
|
"""
|
|
existing = connections.get(id(sender))
|
|
if existing is not None:
|
|
return existing.get(signal, [])
|
|
return []
|
|
|
|
def liveReceivers(receivers):
|
|
"""Filter sequence of receivers to get resolved, live receivers
|
|
|
|
This is a generator which will iterate over
|
|
the passed sequence, checking for weak references
|
|
and resolving them, then returning all live
|
|
receivers.
|
|
"""
|
|
for receiver in receivers:
|
|
if isinstance(receiver, WEAKREF_TYPES):
|
|
# Dereference the weak reference.
|
|
receiver = receiver()
|
|
if receiver is not None:
|
|
yield receiver
|
|
else:
|
|
yield receiver
|
|
|
|
|
|
|
|
def getAllReceivers(sender=Any, signal=Any):
|
|
"""Get list of all receivers from global tables
|
|
|
|
This gets all dereferenced receivers which should receive
|
|
the given signal from sender, each receiver should
|
|
be produced only once by the resulting generator
|
|
"""
|
|
receivers = {}
|
|
# Get receivers that receive *this* signal from *this* sender.
|
|
# Add receivers that receive *any* signal from *this* sender.
|
|
# Add receivers that receive *this* signal from *any* sender.
|
|
# Add receivers that receive *any* signal from *any* sender.
|
|
l = []
|
|
i = id(sender)
|
|
if i in connections:
|
|
sender_receivers = connections[i]
|
|
if signal in sender_receivers:
|
|
l.extend(sender_receivers[signal])
|
|
if signal is not Any and Any in sender_receivers:
|
|
l.extend(sender_receivers[Any])
|
|
|
|
if sender is not Any:
|
|
i = id(Any)
|
|
if i in connections:
|
|
sender_receivers = connections[i]
|
|
if sender_receivers is not None:
|
|
if signal in sender_receivers:
|
|
l.extend(sender_receivers[signal])
|
|
if signal is not Any and Any in sender_receivers:
|
|
l.extend(sender_receivers[Any])
|
|
|
|
for receiver in l:
|
|
try:
|
|
if not receiver in receivers:
|
|
if isinstance(receiver, WEAKREF_TYPES):
|
|
receiver = receiver()
|
|
# this should only (rough guess) be possible if somehow, deref'ing
|
|
# triggered a wipe.
|
|
if receiver is None:
|
|
continue
|
|
receivers[receiver] = 1
|
|
yield receiver
|
|
except TypeError:
|
|
# dead weakrefs raise TypeError on hash...
|
|
pass
|
|
|
|
def send(signal=Any, sender=Anonymous, *arguments, **named):
|
|
"""Send signal from sender to all connected receivers.
|
|
|
|
signal -- (hashable) signal value, see connect for details
|
|
|
|
sender -- the sender of the signal
|
|
|
|
if Any, only receivers registered for Any will receive
|
|
the message.
|
|
|
|
if Anonymous, only receivers registered to receive
|
|
messages from Anonymous or Any will receive the message
|
|
|
|
Otherwise can be any python object (normally one
|
|
registered with a connect if you actually want
|
|
something to occur).
|
|
|
|
arguments -- positional arguments which will be passed to
|
|
*all* receivers. Note that this may raise TypeErrors
|
|
if the receivers do not allow the particular arguments.
|
|
Note also that arguments are applied before named
|
|
arguments, so they should be used with care.
|
|
|
|
named -- named arguments which will be filtered according
|
|
to the parameters of the receivers to only provide those
|
|
acceptable to the receiver.
|
|
|
|
Return a list of tuple pairs [(receiver, response), ... ]
|
|
|
|
if any receiver raises an error, the error propagates back
|
|
through send, terminating the dispatch loop, so it is quite
|
|
possible to not have all receivers called if a raises an
|
|
error.
|
|
"""
|
|
# Call each receiver with whatever arguments it can accept.
|
|
# Return a list of tuple pairs [(receiver, response), ... ].
|
|
responses = []
|
|
for receiver in getAllReceivers(sender, signal):
|
|
response = robustapply.robustApply(
|
|
receiver,
|
|
signal=signal,
|
|
sender=sender,
|
|
*arguments,
|
|
**named
|
|
)
|
|
responses.append((receiver, response))
|
|
return responses
|
|
|
|
|
|
def sendExact(signal=Any, sender=Anonymous, *arguments, **named ):
|
|
"""Send signal only to those receivers registered for exact message
|
|
|
|
sendExact allows for avoiding Any/Anonymous registered
|
|
handlers, sending only to those receivers explicitly
|
|
registered for a particular signal on a particular
|
|
sender.
|
|
"""
|
|
responses = []
|
|
for receiver in liveReceivers(getReceivers(sender, signal)):
|
|
response = robustapply.robustApply(
|
|
receiver,
|
|
signal=signal,
|
|
sender=sender,
|
|
*arguments,
|
|
**named
|
|
)
|
|
responses.append((receiver, response))
|
|
return responses
|
|
|
|
|
|
def _removeReceiver(receiver):
|
|
"""Remove receiver from connections."""
|
|
if not sendersBack:
|
|
# During module cleanup the mapping will be replaced with None
|
|
return False
|
|
backKey = id(receiver)
|
|
for senderkey in sendersBack.get(backKey,()):
|
|
try:
|
|
signals = connections[senderkey].keys()
|
|
except KeyError,err:
|
|
pass
|
|
else:
|
|
for signal in signals:
|
|
try:
|
|
receivers = connections[senderkey][signal]
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
try:
|
|
receivers.remove(receiver)
|
|
except Exception, err:
|
|
pass
|
|
_cleanupConnections(senderkey, signal)
|
|
try:
|
|
del sendersBack[ backKey ]
|
|
except KeyError:
|
|
pass
|
|
|
|
def _cleanupConnections(senderkey, signal):
|
|
"""Delete any empty signals for senderkey. Delete senderkey if empty."""
|
|
try:
|
|
receivers = connections[senderkey][signal]
|
|
except:
|
|
pass
|
|
else:
|
|
if not receivers:
|
|
# No more connected receivers. Therefore, remove the signal.
|
|
try:
|
|
signals = connections[senderkey]
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
del signals[signal]
|
|
if not signals:
|
|
# No more signal connections. Therefore, remove the sender.
|
|
_removeSender(senderkey)
|
|
|
|
def _removeSender(senderkey):
|
|
"""Remove senderkey from connections."""
|
|
_removeBackrefs(senderkey)
|
|
|
|
connections.pop(senderkey, None)
|
|
senders.pop(senderkey, None)
|
|
|
|
|
|
def _removeBackrefs(senderkey):
|
|
"""Remove all back-references to this senderkey"""
|
|
for receiver_list in connections.pop(senderkey, {}).values():
|
|
for receiver in receiver_list:
|
|
_killBackref(receiver, senderkey)
|
|
|
|
|
|
def _removeOldBackRefs(senderkey, signal, receiver, receivers):
|
|
"""Kill old sendersBack references from receiver
|
|
|
|
This guards against multiple registration of the same
|
|
receiver for a given signal and sender leaking memory
|
|
as old back reference records build up.
|
|
|
|
Also removes old receiver instance from receivers
|
|
"""
|
|
try:
|
|
index = receivers.index(receiver)
|
|
# need to scan back references here and remove senderkey
|
|
except ValueError:
|
|
return False
|
|
else:
|
|
oldReceiver = receivers[index]
|
|
del receivers[index]
|
|
found = 0
|
|
signals = connections.get(signal)
|
|
if signals is not None:
|
|
for sig,recs in connections.get(signal,{}).iteritems():
|
|
if sig != signal:
|
|
for rec in recs:
|
|
if rec is oldReceiver:
|
|
found = 1
|
|
break
|
|
if not found:
|
|
_killBackref(oldReceiver, senderkey)
|
|
return True
|
|
return False
|
|
|
|
|
|
def _killBackref(receiver, senderkey):
|
|
"""Do the actual removal of back reference from receiver to senderkey"""
|
|
receiverkey = id(receiver)
|
|
receivers_list = sendersBack.get(receiverkey, ())
|
|
while senderkey in receivers_list:
|
|
try:
|
|
receivers_list.remove(senderkey)
|
|
except:
|
|
break
|
|
if not receivers_list:
|
|
try:
|
|
del sendersBack[ receiverkey ]
|
|
except KeyError:
|
|
pass
|
|
return True
|