"""Base event API.""" from sqlalchemy import util, exc CANCEL = util.symbol('CANCEL') NO_RETVAL = util.symbol('NO_RETVAL') def listen(target, identifier, fn, *args, **kw): """Register a listener function for the given target. """ for evt_cls in _registrars[identifier]: tgt = evt_cls._accept_with(target) if tgt is not None: tgt.dispatch._listen(tgt, identifier, fn, *args, **kw) return raise exc.InvalidRequestError("No such event %s for target %s" % (identifier,target)) def remove(target, identifier, fn): """Remove an event listener. Note that some event removals, particularly for those event dispatchers which create wrapper functions and secondary even listeners, may not yet be supported. """ for evt_cls in _registrars[identifier]: for tgt in evt_cls._accept_with(target): tgt.dispatch._remove(identifier, tgt, fn, *args, **kw) return _registrars = util.defaultdict(list) class _UnpickleDispatch(object): """Serializable callable that re-generates an instance of :class:`_Dispatch` given a particular :class:`.Events` subclass. """ def __call__(self, parent_cls): return parent_cls.__dict__['dispatch'].dispatch_cls(parent_cls) class _Dispatch(object): """Mirror the event listening definitions of an Events class with listener collections. Classes which define a "dispatch" member will return a non-instantiated :class:`._Dispatch` subclass when the member is accessed at the class level. When the "dispatch" member is accessed at the instance level of its owner, an instance of the :class:`._Dispatch` class is returned. A :class:`._Dispatch` class is generated for each :class:`.Events` class defined, by the :func:`._create_dispatcher_class` function. The original :class:`.Events` classes remain untouched. This decouples the construction of :class:`.Events` subclasses from the implementation used by the event internals, and allows inspecting tools like Sphinx to work in an unsurprising way against the public API. """ def __init__(self, parent_cls): self.parent_cls = parent_cls def __reduce__(self): return _UnpickleDispatch(), (self.parent_cls, ) @property def descriptors(self): return (getattr(self, k) for k in dir(self) if k.startswith("on_")) def update(self, other, only_propagate=True): """Populate from the listeners in another :class:`_Dispatch` object.""" for ls in other.descriptors: getattr(self, ls.name).update(ls, only_propagate=only_propagate) class _EventMeta(type): """Intercept new Event subclasses and create associated _Dispatch classes.""" def __init__(cls, classname, bases, dict_): _create_dispatcher_class(cls, classname, bases, dict_) return type.__init__(cls, classname, bases, dict_) def _create_dispatcher_class(cls, classname, bases, dict_): """Create a :class:`._Dispatch` class corresponding to an :class:`.Events` class.""" # there's all kinds of ways to do this, # i.e. make a Dispatch class that shares the '_listen' method # of the Event class, this is the straight monkeypatch. dispatch_base = getattr(cls, 'dispatch', _Dispatch) cls.dispatch = dispatch_cls = type("%sDispatch" % classname, (dispatch_base, ), {}) dispatch_cls._listen = cls._listen dispatch_cls._clear = cls._clear for k in dict_: if k.startswith('on_'): setattr(dispatch_cls, k, _DispatchDescriptor(dict_[k])) _registrars[k].append(cls) def _remove_dispatcher(cls): for k in dir(cls): if k.startswith('on_'): _registrars[k].remove(cls) if not _registrars[k]: del _registrars[k] class Events(object): """Define event listening functions for a particular target type.""" __metaclass__ = _EventMeta @classmethod def _accept_with(cls, target): # Mapper, ClassManager, Session override this to # also accept classes, scoped_sessions, sessionmakers, etc. if hasattr(target, 'dispatch') and ( isinstance(target.dispatch, cls.dispatch) or \ isinstance(target.dispatch, type) and \ issubclass(target.dispatch, cls.dispatch) ): return target else: return None @classmethod def _listen(cls, target, identifier, fn, propagate=False): getattr(target.dispatch, identifier).append(fn, target, propagate) @classmethod def _remove(cls, target, identifier, fn): getattr(target.dispatch, identifier).remove(fn, target) @classmethod def _clear(cls): for attr in dir(cls.dispatch): if attr.startswith("on_"): getattr(cls.dispatch, attr).clear() class _DispatchDescriptor(object): """Class-level attributes on :class:`._Dispatch` classes.""" def __init__(self, fn): self.__name__ = fn.__name__ self.__doc__ = fn.__doc__ self._clslevel = util.defaultdict(list) def append(self, obj, target, propagate): assert isinstance(target, type), \ "Class-level Event targets must be classes." for cls in [target] + target.__subclasses__(): self._clslevel[cls].append(obj) def remove(self, obj, target): for cls in [target] + target.__subclasses__(): self._clslevel[cls].remove(obj) def clear(self): """Clear all class level listeners""" for dispatcher in self._clslevel.values(): dispatcher[:] = [] def __get__(self, obj, cls): if obj is None: return self obj.__dict__[self.__name__] = result = \ _ListenerCollection(self, obj.parent_cls) return result class _ListenerCollection(object): """Instance-level attributes on instances of :class:`._Dispatch`. Represents a collection of listeners. """ _exec_once = False def __init__(self, parent, target_cls): self.parent_listeners = parent._clslevel[target_cls] self.name = parent.__name__ self.listeners = [] self.propagate = set() def exec_once(self, *args, **kw): """Execute this event, but only if it has not been executed already for this collection.""" if not self._exec_once: self(*args, **kw) self._exec_once = True def __call__(self, *args, **kw): """Execute this event.""" for fn in self.parent_listeners: fn(*args, **kw) for fn in self.listeners: fn(*args, **kw) # I'm not entirely thrilled about the overhead here, # but this allows class-level listeners to be added # at any point. # # alternatively, _DispatchDescriptor could notify # all _ListenerCollection objects, but then we move # to a higher memory model, i.e.weakrefs to all _ListenerCollection # objects, the _DispatchDescriptor collection repeated # for all instances. def __len__(self): return len(self.parent_listeners + self.listeners) def __iter__(self): return iter(self.parent_listeners + self.listeners) def __getitem__(self, index): return (self.parent_listeners + self.listeners)[index] def __nonzero__(self): return bool(self.listeners or self.parent_listeners) def update(self, other, only_propagate=True): """Populate from the listeners in another :class:`_Dispatch` object.""" existing_listeners = self.listeners existing_listener_set = set(existing_listeners) self.propagate.update(other.propagate) existing_listeners.extend([l for l in other.listeners if l not in existing_listener_set and not only_propagate or l in self.propagate ]) def append(self, obj, target, propagate): if obj not in self.listeners: self.listeners.append(obj) if propagate: self.propagate.add(obj) def remove(self, obj, target): if obj in self.listeners: self.listeners.remove(obj) self.propagate.discard(obj) def clear(self): self.listeners[:] = [] self.propagate.clear() class dispatcher(object): """Descriptor used by target classes to deliver the _Dispatch class at the class level and produce new _Dispatch instances for target instances. """ def __init__(self, events): self.dispatch_cls = events.dispatch self.events = events def __get__(self, obj, cls): if obj is None: return self.dispatch_cls obj.__dict__['dispatch'] = disp = self.dispatch_cls(cls) return disp