Source code for enaml.signaling
#------------------------------------------------------------------------------
# Copyright (c) 2012, Enthought, Inc.
# All rights reserved.
#------------------------------------------------------------------------------
from types import MethodType
from weakref import ref
from .callableref import CallableRef
from .weakmethod import WeakMethod
#: The key used to store the signals in an object's __dict__
_SIGNALS_KEY = '_[signals]'
[docs]class Signal(object):
""" A descriptor which provides notification functionality similar
to Qt signals and slots.
A Signal is used by creating an instance in the body of a class
definition. Slots (callables) are connected to the signal through
the `connect` and `disconnect` methods. A signal can be emitted by
calling the `emit` method passing arbitrary positional and keyword
arguments.
If a bound method is connected to a signal, then that slot will be
automatically disconnected when the underlying object instance is
garbage collected.
"""
__slots__ = ()
@staticmethod
def disconnect_all(obj):
""" Disconnect all slots connected to all signals on an object.
Parameters
----------
obj : object
An object which has signals. Any slots connected to signals
on this object will be disconnected.
"""
dct = obj.__dict__
key = _SIGNALS_KEY
if key in dct:
del dct[key]
def __get__(self, obj, cls):
""" The data descriptor getter.
Returns
-------
result : Signal or BoundSignal
If the descriptor is accessed through the class, then this
Signal will be returned. Otherwise, a BoundSignal for the
object will be returned.
"""
if obj is None:
return self
return BoundSignal(self, ref(obj))
def __set__(self, obj, val):
""" The data descriptor setter.
Attempting to assign to a Signal will raise an AttributeError.
"""
raise AttributeError("can't set read only Signal")
def __delete__(self, obj):
""" The data descriptor deleter.
This will disconnect all slots connected to this signal owned
by the given object.
"""
dct = obj.__dict__
key = _SIGNALS_KEY
if key not in dct:
return
signals = dct[key]
if self not in signals:
return
del signals[self]
if len(signals) == 0:
del dct[key]
class _Disconnector(object):
""" An object which disconnects a slot from a signal when the slot
is garbage collected.
This class is a private implementation detail of signaling and is
not meant for public consumption.
"""
__slots__ = ('_signal', '_objref')
def __init__(self, signal, objref):
""" Initialize a _Disconnector.
Parameters
----------
signal : Signal
The Signal instance associated with this disconnector.
objref : weakref
A weak reference to the object which owns the signal.
"""
self._signal = signal
self._objref = objref
def __call__(self, slot):
""" Disconnect the slot from the signal.
Parameters
----------
slot : callable
The slot to be disconnected from the signal.
"""
obj = self._objref()
if obj is None:
return
key = _SIGNALS_KEY
dct = obj.__dict__
if key not in dct:
return
signals = dct[key]
signal = self._signal
if signal not in signals:
return
slots = signals[signal]
try:
slots.remove(slot)
except ValueError:
pass
else:
# A _Disconnector is the first item in the list and is
# created on demand. The list is deleted when that is the
# only item remaining.
if len(slots) == 1:
del signals[signal]
if len(signals) == 0:
del dct[key]
[docs]class BoundSignal(object):
""" A bound Signal object.
Instances of this class are created on the fly by a Signal. This
class performs the actual work for connecting, disconnecting, and
emitting signals.
"""
__slots__ = ('_signal', '_objref')
[docs] def __init__(self, signal, objref):
""" Initialize a BoundSignal.
Parameters
----------
signal : Signal
The Signal instance which created this BoundSignal.
objref : weakref
A weak reference to the object which owns the signal. The
weakref should not have been created with a callback, as
the internal implementation depends on the semantics of
weakrefs created without callbacks.
"""
self._signal = signal
self._objref = objref
[docs] def __eq__(self, other):
""" Custom equality checking for a BoundSignal.
A BoundSignal will compare equal to another BoundSignal if the
unerlying Signal and object reference are equal. This equality
is part of the mechanism which allows a signal to be connected
to another signal.
"""
if isinstance(other, BoundSignal):
signal = self._signal
objref = self._objref
return signal is other._signal and objref is other._objref
return False
[docs] def __call__(self, *args, **kwargs):
""" Custom call support for a BoundSignal.
Calling a signal is indentical invoking the `emit` method. By
making a signal callable, it is possible to directly connect
a signal to another signal.
Parameters
----------
*args, **kwargs
The positional and keyword arguments to pass to the slots
connected to the signal.
"""
self.emit(*args, **kwargs)
#--------------------------------------------------------------------------
# Public API
#--------------------------------------------------------------------------
[docs] def emit(self, *args, **kwargs):
""" Emit the signal with the given arguments and keywords.
If a connected slot raises an exceptions, no further slots will
be invoked and the exception will be bubbled up.
Parameters
----------
*args, **kwargs
The positional and keyword arguments to pass to the slots
connected to the signal.
"""
obj = self._objref()
if obj is None:
return
dct = obj.__dict__
key = _SIGNALS_KEY
if key not in dct:
return
signals = dct[key]
signal = self._signal
if signal not in signals:
return
# Make a copy of the list of slots since calling a slot has the
# potential to modify the slots list. The first item in the list
# is skipped since it is the disconnector for the signal. Putting
# the disconnector in the list saves time and space.
slots = signals[signal][1:]
for slot in slots:
slot(*args, **kwargs)
[docs] def connect(self, slot):
""" Connect the given slot to the signal.
The slot will be called when the signal is emitted. It will be
passed any positional and keyword arguments that were emitted
with the signal. Multiple slots connected to a signal will be
invoked in the order in which they were connected. Slots which
are instance methods will be automatically disconnected when
their underlying instance is garbage collected.
Parameters
----------
slot : callable
The callable slot to invoke when the signal is emitted.
"""
obj = self._objref()
if obj is None:
return
dct = obj.__dict__
key = _SIGNALS_KEY
if key in dct:
signals = dct[key]
else:
signals = dct[key] = {}
signal = self._signal
if signal in signals:
slots = signals[signal]
d = slots[0]
else:
d = _Disconnector(signal, self._objref)
slots = signals[signal] = [d]
if isinstance(slot, MethodType) and slot.im_self is not None:
slot = CallableRef(WeakMethod(slot), d)
slots.append(slot)
[docs] def disconnect(self, slot):
""" Disconnect the slot from the signal.
If the slot was not previously connected, this is a no-op.
Parameters
----------
slot : callable
The callable slot to disconnect from the signal.
"""
if isinstance(slot, MethodType) and slot.im_self is not None:
slot = CallableRef(WeakMethod(slot))
_Disconnector(self._signal, self._objref)(slot)
# Use the faster version of signaling if it's available.
try:
from enaml.extensions.signaling import Signal, BoundSignal, _Disconnector
except ImportError:
pass