--- /dev/null
+"""
+The event system handles all events throughout the sqlalchemy
+and sqlalchemy.orm packages.
+
+Event specifications:
+
+:attr:`sqlalchemy.pool.Pool.events`
+
+"""
+
+from sqlalchemy import util
+
+def listen(fn, identifier, target, *args, **kw):
+ """Listen for events, passing to fn."""
+
+ # rationale - the events on ClassManager, Session, and Mapper
+ # will need to accept mapped classes directly as targets and know
+ # what to do
+
+ for evt_cls in _registrars[identifier]:
+ for tgt in evt_cls.accept_with(target):
+ tgt.events.listen(fn, identifier, tgt, *args, **kw)
+ break
+
+class _DispatchMeta(type):
+ def __init__(cls, classname, bases, dict_):
+ for k in dict_:
+ if k.startswith('on_'):
+ setattr(cls, k, EventDescriptor(dict_[k]))
+ _registrars[k].append(cls)
+ return type.__init__(cls, classname, bases, dict_)
+
+_registrars = util.defaultdict(list)
+
+class Events(object):
+ __metaclass__ = _DispatchMeta
+
+ def __init__(self, parent_cls):
+ self.parent_cls = parent_cls
+
+ @classmethod
+ def accept_with(cls, target):
+ # Mapper, ClassManager, Session override this to
+ # also accept classes, scoped_sessions, sessionmakers, etc.
+ if hasattr(target, 'events') and (
+ isinstance(target.events, cls) or \
+ isinstance(target.events, type) and \
+ issubclass(target.events, cls)
+ ):
+ return [target]
+ else:
+ return []
+
+ @classmethod
+ def listen(cls, fn, identifier, target):
+ getattr(target.events, identifier).append(fn, target)
+
+ @property
+ def events(self):
+ """Iterate the Listeners objects."""
+
+ return (getattr(self, k) for k in dir(self) if k.startswith("on_"))
+
+ def update(self, other):
+ """Populate from the listeners in another :class:`Events` object."""
+
+ for ls in other.events:
+ getattr(self, ls.name).listeners.extend(ls.listeners)
+
+class _ExecEvent(object):
+ _exec_once = False
+
+ 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 exec_until_return(self, *args, **kw):
+ """Execute listeners for this event until
+ one returns a non-None value.
+
+ Returns the value, or None.
+ """
+
+ if self:
+ for fn in self:
+ r = fn(*args, **kw)
+ if r is not None:
+ return r
+ return None
+
+ def __call__(self, *args, **kw):
+ """Execute this event."""
+
+ if self:
+ for fn in self:
+ fn(*args, **kw)
+
+class EventDescriptor(object):
+ """Represent an event type associated with a :class:`Events` class
+ as well as class-level listeners.
+
+ """
+ def __init__(self, fn):
+ self.__name__ = fn.__name__
+ self.__doc__ = fn.__doc__
+ self._clslevel = util.defaultdict(list)
+
+ def append(self, obj, target):
+ assert isinstance(target, type), "Class-level Event targets must be classes."
+ for cls in [target] + target.__subclasses__():
+ self._clslevel[cls].append(obj)
+
+ def __get__(self, obj, cls):
+ if obj is None:
+ return self
+ obj.__dict__[self.__name__] = result = Listeners(self, obj.parent_cls)
+ return result
+
+class Listeners(_ExecEvent):
+ """Represent a collection of listeners linked
+ to an instance of :class:`Events`."""
+
+ def __init__(self, parent, target_cls):
+ self.parent_listeners = parent._clslevel[target_cls]
+ self.name = parent.__name__
+ self.listeners = []
+
+ # I'm not entirely thrilled about the overhead here,
+ # but this allows class-level listeners to be added
+ # at any point.
+
+ 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 append(self, obj, target):
++ # this will be needed, but not
++ # sure why we don't seem to need it yet
++ # if obj not in self.listeners:
+ self.listeners.append(obj)
+
+class dispatcher(object):
+ def __init__(self, events):
+ self.dispatch_cls = events
+
+ def __get__(self, obj, cls):
+ if obj is None:
+ return self.dispatch_cls
+ obj.__dict__['events'] = disp = self.dispatch_cls(cls)
+ return disp
self.comparator = comparator
self.parententity = parententity
- # TODO: this can potentially be moved to AttributeImpl,
- # have Sphinx document the "events" class directly, implement an
- # accept_with() that checks for QueryableAttribute
+ class events(event.Events):
+ """Events for ORM attributes.
+
+ e.g.::
+
+ from sqlalchemy import event
+ event.listen(listener, 'on_append', MyClass.collection)
+ event.listen(listener, 'on_set', MyClass.some_scalar, active_history=True)
+
+ active_history = True indicates that the "on_set" event would like
+ to receive the 'old' value, even if it means firing lazy callables.
+
+ """
+
+ active_history = False
+
++ # TODO: what to do about subclasses !!
++ # a shared approach will be needed. listeners can be placed
++ # before subclasses are created. new attrs on subclasses
++ # can pull them from the superclass attr. listeners
++ # should be auto-propagated to existing subclasses.
++
+ @classmethod
+ def listen(cls, fn, identifier, target, active_history=False):
+ if active_history:
+ target.events.active_history = True
+ event.Events.listen(fn, identifier, target)
+
+ def on_append(self, state, value, initiator):
+ """Receive a collection append event.
+
+ The returned value will be used as the actual value to be
+ appended.
+
+ """
+
+ def on_remove(self, state, value, initiator):
+ """Receive a remove event.
+
+ No return value is defined.
+
+ """
+
+ def on_set(self, state, value, oldvalue, initiator):
+ """Receive a set event.
+
+ The returned value will be used as the actual value to be
+ set.
+
+ """
+
+ events = event.dispatcher(events)
+
def get_history(self, instance, **kwargs):
return self.impl.get_history(instance_state(instance),
instance_dict(instance), **kwargs)
self.is_equal = operator.eq
else:
self.is_equal = compare_function
- self.extensions = util.to_list(extension or [])
- for e in self.extensions:
- if e.active_history:
- active_history = True
- break
- self.active_history = active_history
- self.expire_missing = expire_missing
- attr = getattr(class_, key)
++ # TODO: pass in the manager here
++ # instead of doing a lookup
++ attr = manager_of_class(class_)[key]
+
+ for ext in util.to_list(extension or []):
+ ext._adapt_listener(attr, ext)
+
+ if active_history:
+ events.active_history = True
+
+ self.expire_missing = expire_missing
+
-
def hasparent(self, state, optimistic=False):
"""Return the boolean value of a `hasparent` flag attached to
the given state.