2021-08-30 14:33:37 +08:00

542 lines
15 KiB
Python

# -*- coding: utf-8; fill-column: 76 -*-
import gc
__all__ = ["signal", "Signal"]
class attrgetter:
__slots__ = ('_attrs', '_call')
def __init__(self, attr, *attrs):
if not attrs:
if not isinstance(attr, str):
raise TypeError('attribute name must be a string')
self._attrs = (attr,)
names = attr.split('.')
def func(obj):
for name in names:
obj = getattr(obj, name)
return obj
self._call = func
else:
self._attrs = (attr,) + attrs
getters = tuple(map(attrgetter, self._attrs))
def func(obj):
return tuple(getter(obj) for getter in getters)
self._call = func
def __call__(self, obj):
return self._call(obj)
def __repr__(self):
return '%s.%s(%s)' % (self.__class__.__module__,
self.__class__.__qualname__,
', '.join(map(repr, self._attrs)))
class ReferenceType(object):
def __new__(cls, *args, **kwargs):
return super(ReferenceType, cls).__new__(cls)
def __call__(self, *args, **kwargs):
return
def __init__(self, *args, **kwargs):
self.params = dict()
def __setitem__(self, key, value):
self.__dict__[key] = value
def __getitem__(self, key):
if key not in self.__dict__:
self.__dict__[key] = None
return self.__dict__[key]
__callback__ = property(lambda self: object(), lambda self, v: None, lambda self: None) # default
ref = ReferenceType
class KeyedRef(ref):
__slots__ = "key",
def __new__(type, ob, callback, key):
self = ref.__new__(type, ob, callback)
self.key = key
return self
def __init__(self, ob, callback, key):
super().__init__(ob, callback)
class WeakValueDictionary(dict):
def __init__(self, dict=None):
self.data = {}
def remove(k, selfref=ref(self)):
self = selfref()
if self is not None:
if self._iterating:
self._pending_removals.append(k)
else:
del self.data[k]
self._remove = remove
self._pending_removals = []
self._iterating = set()
self._dirty_len = False
if dict is not None:
self.update(dict)
def _commit_removals(self):
gc.collect()
def __getitem__(self, key):
if self._pending_removals:
self._commit_removals()
o = self.data[key]()
if o is None:
raise KeyError(key)
else:
return o
def __setitem__(self, key, value):
if self._pending_removals:
self._commit_removals()
self.data[key] = KeyedRef(value, self._remove, key)
def get(self, key, default=None):
if self._pending_removals:
self._commit_removals()
try:
wr = self.data[key]
except KeyError:
return default
else:
o = wr()
if o is None:
return default
else:
return o
def update(self, other=None, **kwargs):
if self._pending_removals:
self._commit_removals()
d = self.data
if other is not None:
if not hasattr(other, "items"):
other = dict(other)
for key, o in other.items():
d[key] = KeyedRef(o, self._remove, key)
for key, o in kwargs.items():
d[key] = KeyedRef(o, self._remove, key)
try:
callable
except NameError:
def callable(object):
return hasattr(object, '__call__')
get_self = attrgetter('__self__')
get_func = attrgetter('__func__')
class BoundMethodWeakref(object):
_all_instances = WeakValueDictionary()
def __new__(cls, target, on_delete=None, *arguments, **named):
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):
def remove(self=self):
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 as e:
print('Exception during saferef %s '
'cleanup function %s: %s' % (self, function, e))
self.deletion_methods = [on_delete]
self.key = self.calculate_key(target)
im_self = get_self(target)
im_func = get_func(target)
self.weak_self = ref(im_self, remove)
self.weak_func = ref(im_func, remove)
self.self_name = str(im_self)
self.func_name = str(im_func.__name__)
def calculate_key(cls, target):
return (id(get_self(target)), id(get_func(target)))
calculate_key = classmethod(calculate_key)
def __str__(self):
return "%s(%s.%s)" % (
self.__class__.__name__,
self.self_name,
self.func_name,
)
__repr__ = __str__
def __call__(self):
target = self.weak_self()
if target is not None:
function = self.weak_func()
if function is not None:
return function.__get__(target)
return None
class defaultdict(object):
params = dict()
def __init__(self, default_factory=None, *a, **kw):
super().__init__(*a, **kw)
self.default_factory = default_factory
def __getitem__(self, key):
try:
result = self.params[key]
return result
except KeyError:
return self.__missing__(key)
def __setitem__(self, key, value):
try:
self.params[key] = set(value)
except TypeError:
return "Type error"
def __missing__(self, key):
if self.default_factory is None:
raise KeyError(key)
self.params[key] = value = self.default_factory()
return value
def __reduce__(self):
if self.default_factory is None:
args = tuple()
else:
args = self.default_factory,
return type(self), args, None, None, self.items()
def copy(self):
return self.__copy__()
def pop(self, key, default):
return self.params.pop(key, default)
def values(self):
return self.params.values()
def items(self):
return self.params.items()
def clear(self):
return self.params.clear()
def __copy__(self):
return type(self)(self.default_factory, self)
def __repr__(self):
return 'defaultdict(%s, %s)' % (self.default_factory,
dict.__repr__(self))
def __contains__(self, key):
if key in self.params:
return True
else:
return False
class _symbol(object):
def __init__(self, name):
self.__name__ = self.name = name
def __reduce__(self):
return symbol, (self.name,)
def __repr__(self):
return self.name
_symbol.__name__ = 'symbol'
class symbol(object):
symbols = {}
def __new__(cls, name):
try:
return cls.symbols[name]
except KeyError:
return cls.symbols.setdefault(name, _symbol(name))
try:
text = (str, unicode)
except NameError:
text = str
def hashable_identity(obj):
if hasattr(obj, '__func__'):
return (id(obj.__func__), id(obj.__self__))
elif isinstance(obj, text):
return obj
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):
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):
if hasattr(object, 'im_self') and object.im_self is not None:
return BoundMethodWeakref(target=object, on_delete=callback)
elif hasattr(object, '__self__') and object.__self__ is not None:
return BoundMethodWeakref(target=object, on_delete=callback)
return annotatable_weakref(object, callback)
ANY = symbol('ANY')
ANY.__doc__ = 'Token for "any sender".'
ANY_ID = 0
class Signal(object):
ANY = ANY
def receiver_connected(self):
return Signal(doc="Emitted after a receiver connects.")
def receiver_disconnected(self):
return Signal(doc="Emitted after a receiver disconnects.")
def __init__(self, doc=None):
self.receivers = {}
self._by_receiver = defaultdict(default_factory=set)
self._by_sender = defaultdict(default_factory=set)
self._weak_senders = {}
def __repr__(self):
return "<%s at %#x>" % (self.__class__.__name__, id(self))
def connect(self, receiver, sender=ANY, weak=False):
receiver_id = hashable_identity(receiver)
weak = False
receiver_ref = receiver
if sender is ANY:
sender_id = ANY_ID
else:
sender_id = 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:
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
if ('receiver_connected' in self.__dict__ and
self.receiver_connected.receivers):
try:
self.receiver_connected.send(self,
receiver=receiver,
sender=sender,
weak=weak)
except:
self.disconnect(receiver, sender)
raise
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 connect_via(self, sender=ANY, weak=False):
def decorator(fn):
self.connect(fn, sender, weak)
return fn
return decorator
def send(self, *sender, **kwargs):
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):
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):
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):
if sender is ANY:
sender_id = ANY_ID
else:
sender_id = hashable_identity(sender)
receiver_id = hashable_identity(receiver)
self._disconnect(receiver_id, sender_id)
if ('receiver_disconnected' in self.__dict__ and
self.receiver_disconnected.receivers):
self.receiver_disconnected.send(self,
receiver=receiver,
sender=sender)
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)
self._by_receiver[receiver_id].discard(sender_id)
def _cleanup_receiver(self, receiver_ref):
self._disconnect(receiver_ref.receiver_id, ANY_ID)
def _cleanup_sender(self, sender_ref):
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 _cleanup_bookkeeping(self):
for mapping in (self._by_sender, self._by_receiver):
for _id, bucket in list(mapping.items()):
if not bucket:
mapping.pop(_id, None)
def _clear_state(self):
self._weak_senders.clear()
self.receivers.clear()
self._by_sender.clear()
self._by_receiver.clear()
receiver_connected = Signal("""\
Sent by a :class:`Signal` after a receiver connects.
""")
class NamedSignal(Signal):
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(dict):
def signal(self, name, doc=None):
try:
return self[name]
except KeyError:
return self.setdefault(name, NamedSignal(name, doc))
signal = Namespace().signal
if __name__ == '__main__':
dice_roll = signal('dice_roll')
def odd_subscriber(sender, **kwargs):
print("Observed dice roll %r. %r" % (sender, kwargs))
dice_roll.connect(odd_subscriber, sender=0x03)
result = dice_roll.send(3)