mirror of
https://gitee.com/quecpython/helios-service.git
synced 2025-05-19 06:08:22 +08:00
542 lines
15 KiB
Python
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)
|