# -*- coding: utf-8 -*-
from __future__ import absolute_import
import weakref
import functools
from ...compat import WeakOrderedSet, WeakSet
from ...utils.callbacks import Callbacks
from ...utils import ReferenceError
__all__ = ['_DescriptorEvent', '_KeywordEvent', '_KeywordListener']
[docs]class _DescriptorEvent(object):
"""Manage events attached to a keyword descriptor."""
def __init__(self, name, replace_method=False):
super(_DescriptorEvent, self).__init__()
self.name = name
self.replace_method = replace_method
self.callbacks = Callbacks()
[docs] def listen(self, func):
"""Listen to a function."""
self.callbacks.add(func)
if self.replace_method and len(self.callbacks) > 1:
raise ValueError("There is more than one replacement for '{0}'".format(self.name))
def __repr__(self):
return "<{0} name={1}>".format(self.__class__.__name__, self.name)
[docs] def propagate(self, instance, keyword, *args, **kwargs):
"""Propagate a listener event through to the keyword."""
returned = None
for callback in self.callbacks:
try:
returned = callback(keyword, *args, **kwargs)
except (TypeError, ReferenceError):
returned = callback.bound(instance)(keyword, *args, **kwargs)
return returned
[docs] def __call__(self, func):
"""Use the event as a descriptor."""
self.listen(func)
return func
[docs]class _KeywordEvent(object):
"""Instrumentation to apply an event to a keyword."""
name = ""
replace_method = False
def __new__(cls, keyword, instance, event):
"""Construct or intercept the construction of a keyword event."""
if isinstance(getattr(keyword, event.name), cls):
return getattr(keyword, event.name)
return super(_KeywordEvent, cls).__new__(cls)
def __init__(self, keyword, instance, event):
super(_KeywordEvent, self).__init__()
# Important implementation note here: this object behaves like a singleton
# due to the override in __new__ above, this method will be called on both
# new objects and already existing objects which are re-bound to a particular
# function. The re-binding allows us to add additional listeners to this
# object on an as-needed basis. However, everything done in this method
# should be ok with multiple invocations.
func = getattr(keyword, event.name)
if func is not self:
self.func = func
setattr(keyword, event.name, self)
functools.update_wrapper(self, func)
if not hasattr(self, 'listeners'):
self.listeners = []
listener = _KeywordListener(keyword, instance, event)
if listener not in self.listeners:
self.listeners.append(listener)
if event.replace_method and self.nlisteners > 1:
raise ValueError("There is more than one method replacement for '{0}'"
" on keyword '{1}'".format(event.name, keyword.name))
self.replace_method |= event.replace_method
self.name = event.name
self.keyword = weakref.ref(keyword)
@property
def nlisteners(self):
"""Number of listening callbacks."""
return sum(len(listener.event.callbacks) for listener in self.listeners)
def __repr__(self):
return "<{0} name={1} at {2}>".format(self.__class__.__name__, self.name, hex(id(self)))
[docs] def __call__(self, *args, **kwargs):
"""This is used to call the underlying method, and to notify listeners."""
_remove = []
returned = None
for callback in self.listeners:
try:
returned = callback(*args, **kwargs)
except ReferenceError:
_remove.append(callback)
continue
for listener in _remove:
self.listeners.remove(listener)
# If there are no listeners left, replace the function.
keyword = self.keyword()
if not len(self.listeners) and keyword is not None:
setattr(keyword, self.name, self.func)
if self.replace_method and self.nlisteners:
return returned
else:
return self.func(*args, **kwargs)
[docs]class _KeywordListener(object):
"""A listener to help fire events on a keyword object."""
def __init__(self, keyword, instance, event):
super(_KeywordListener, self).__init__()
self.event = weakref.proxy(event)
self.keyword = weakref.proxy(keyword)
self.instance = weakref.proxy(instance)
def __repr__(self):
return "<{0} name={1} at {2}>".format(self.__class__.__name__, self.event.name, hex(id(self)))
def __eq__(self, other):
"""Compare to other listeners"""
if not isinstance(other, _KeywordListener):
return NotImplemented
try:
return (self.event == other.event and self.keyword == other.keyword and self.instance == other.instance)
except ReferenceError:
# Listeners are by definition not equal
return False
def __ne__(self, other):
"""Not equal to another listener."""
eq = self.__eq__(other)
if eq is NotImplemented:
return NotImplemented
return (not eq)
[docs] def __call__(self, *args, **kwargs):
"""Ensure that the dispatcher fires before the keyword's own implementation."""
return self.event.propagate(self.instance, self.keyword, *args, **kwargs)