"""
-Illustrates how to use AttributeExtension to listen for change events
-across the board.
+Illustrates how to attach events to all instrumented attributes
+and listen for change events.
"""
-from sqlalchemy.orm.interfaces import AttributeExtension, \
- InstrumentationManager
+from sqlalchemy import event, orm
-class InstallListeners(InstrumentationManager):
- def post_configure_attribute(self, class_, key, inst):
- """Add an event listener to an InstrumentedAttribute."""
-
- inst.impl.extensions.insert(0, AttributeListener(key))
-
-class AttributeListener(AttributeExtension):
- """Generic event listener.
-
- Propagates attribute change events to a
- "receive_change_event()" method on the target
- instance.
-
- """
- def __init__(self, key):
- self.key = key
-
- def append(self, state, value, initiator):
- self._report(state, value, None, "appended")
- return value
+def configure_listener(class_, key, inst):
+ def append(instance, value, initiator):
+ instance.receive_change_event("append", key, value, None)
- def remove(self, state, value, initiator):
- self._report(state, value, None, "removed")
+ def remove(instance, value, initiator):
+ instance.receive_change_event("remove", key, value, None)
+
+ def set_(instance, value, oldvalue, initiator):
+ instance.receive_change_event("set", key, value, oldvalue)
+
+ event.listen(append, 'on_append', inst)
+ event.listen(remove, 'on_remove', inst)
+ event.listen(set_, 'on_set', inst)
- def set(self, state, value, oldvalue, initiator):
- self._report(state, value, oldvalue, "set")
- return value
-
- def _report(self, state, value, oldvalue, verb):
- state.obj().receive_change_event(verb, self.key, value, oldvalue)
if __name__ == '__main__':
from sqlalchemy.ext.declarative import declarative_base
class Base(object):
- __sa_instrumentation_manager__ = InstallListeners
def receive_change_event(self, verb, key, value, oldvalue):
s = "Value '%s' %s on attribute '%s', " % (value, verb, key)
Base = declarative_base(cls=Base)
+ event.listen(configure_listener, 'on_attribute_instrument', Base)
+
class MyMappedClass(Base):
__tablename__ = "mytable"
return
raise exc.InvalidRequestError("No such event %s for target %s" %
(identifier,target))
+
+def remove(fn, identifier, target):
+ """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(fn, identifier, tgt, *args, **kw)
+
+
_registrars = util.defaultdict(list)
class _Dispatch(object):
@classmethod
def listen(cls, fn, identifier, target):
getattr(target.dispatch, identifier).append(fn, target)
-
-# 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)
+ @classmethod
+ def remove(cls, fn, identifier, target):
+ getattr(target.dispatch, identifier).remove(fn, target)
+
class _DispatchDescriptor(object):
"""Class-level attributes on _Dispatch 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 __get__(self, obj, cls):
if obj is None:
return self
def append(self, obj, target):
if obj not in self.listeners:
self.listeners.append(obj)
-
+
+ def remove(self, obj, target):
+ if obj in self.listeners:
+ self.listeners.remove(obj)
+
class dispatcher(object):
"""Descriptor used by target classes to
deliver the _Dispatch class at the class level
from operator import itemgetter
from sqlalchemy import util, event
-from sqlalchemy.orm import interfaces, collections
+from sqlalchemy.orm import interfaces, collections, events
import sqlalchemy.exceptions as sa_exc
# lazy imports
"""Symbol indicating that loader callables should be executed."""
-class AttributeEvents(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.
-
- """
-
- # 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.dispatch.active_history = True
- event.Events.listen(fn, identifier, target)
-
- @classmethod
- def unwrap(cls, identifier, event):
- return event['value']
-
- 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.
-
- """
-
class QueryableAttribute(interfaces.PropComparator):
-
- def __init__(self, key, impl=None, comparator=None, parententity=None):
- """Construct an InstrumentedAttribute.
-
- comparator
- a sql.Comparator to which class-level compare/math events will be
- sent
- """
+ """Base class for class-bound attributes. """
+
+ def __init__(self, class_, key, impl=None, comparator=None, parententity=None):
+ self.class_ = class_
self.key = key
self.impl = impl
self.comparator = comparator
self.parententity = parententity
- dispatch = event.dispatcher(AttributeEvents)
+ dispatch = event.dispatcher(events.AttributeEvents)
dispatch.dispatch_cls.active_history = False
def get_history(self, instance, **kwargs):
class InstrumentedAttribute(QueryableAttribute):
- """Public-facing descriptor, placed in the mapped class dictionary."""
+ """Class bound instrumented attribute which adds descriptor methods."""
def __set__(self, instance, value):
self.impl.set(instance_state(instance),
impl = ScalarAttributeImpl(class_, key, callable_, dispatch, **kw)
manager[key].impl = impl
-
+
manager.post_configure_attribute(key)
proxy_type = proxied_attribute_factory(proxy_property)
descriptor = proxy_type(key, proxy_property, comparator, parententity)
else:
- descriptor = InstrumentedAttribute(key, comparator=comparator,
+ descriptor = InstrumentedAttribute(class_, key, comparator=comparator,
parententity=parententity)
descriptor.__doc__ = doc
@classmethod
def _adapt_listener(cls, self, listener):
event.listen(listener.append, 'on_append', self,
- active_history=listener.active_history)
+ active_history=listener.active_history,
+ raw=True, retval=True)
event.listen(listener.remove, 'on_remove', self,
- active_history=listener.active_history)
+ active_history=listener.active_history,
+ raw=True, retval=True)
event.listen(listener.set, 'on_set', self,
- active_history=listener.active_history)
-
+ active_history=listener.active_history,
+ raw=True, retval=True)
def append(self, state, value, initiator):
"""Receive a collection append event.
"""ORM event interfaces.
"""
-from sqlalchemy import event
+from sqlalchemy import event, util, exc
-class ClassEvents(event.Events):
+class InstrumentationEvents(event.Events):
+ """Events related to class instrumentation events.
+
+ The listeners here support being established against
+ any new style class, that is any object that is a subclass
+ of 'type'. Events will then be fired off for events
+ against that class as well as all subclasses.
+ 'type' itself is also accepted as a target
+ in which case the events fire for all classes.
+
+ """
+
@classmethod
def accept_with(cls, target):
- from sqlalchemy.orm.instrumentation import ClassManager
+ from sqlalchemy.orm.instrumentation import instrumentation_registry
+
+ if isinstance(target, type):
+ return [instrumentation_registry]
+ else:
+ return []
+
+ @classmethod
+ def listen(cls, fn, identifier, target):
+
+ @util.decorator
+ def adapt_to_target(fn, cls, *arg):
+ if issubclass(cls, target):
+ fn(cls, *arg)
+ event.Events.listen(fn, identifier, target)
+
+ @classmethod
+ def remove(cls, fn, identifier, target):
+ raise NotImplementedError("Removal of instrumentation events not yet implemented")
+
+ def on_class_instrument(self, cls):
+ """Called after the given class is instrumented.
+
+ To get at the :class:`.ClassManager`, use
+ :func:`.manager_of_class`.
+
+ """
+
+ def on_class_uninstrument(self, cls):
+ """Called before the given class is uninstrumented.
+
+ To get at the :class:`.ClassManager`, use
+ :func:`.manager_of_class`.
+
+ """
+
+
+ def on_attribute_instrument(self, cls, key, inst):
+ """Called when an attribute is instrumented."""
+
+class InstanceEvents(event.Events):
+
+ @classmethod
+ def accept_with(cls, target):
+ from sqlalchemy.orm.instrumentation import ClassManager, manager_of_class
if isinstance(target, ClassManager):
return [target]
if manager:
return [manager]
return []
+
+ @classmethod
+ def listen(cls, fn, identifier, target, raw=False):
+ if not raw:
+ fn = _to_instance(fn)
+ event.Events.listen(fn, identifier, target)
+
+ @classmethod
+ def remove(cls, fn, identifier, target):
+ raise NotImplementedError("Removal of instance events not yet implemented")
- # TODO: change these to accept "target" -
- # the target is the state or the instance, depending
- # on if the listener was registered with "raw=True" -
- # do the same thing for all the other events here (Mapper, Session, Attributes).
- # Not sending raw=True means the listen() method of the
- # Events subclass will wrap incoming listeners to marshall each
- # "target" argument into "instance". The name "target" can be
- # used consistently to make it simple.
- #
- # this way end users don't have to deal with InstanceState and
- # the internals can have faster performance.
-
- def on_init(self, state, instance, args, kwargs):
+ def on_init(self, target, args, kwargs):
""""""
- def on_init_failure(self, state, instance, args, kwargs):
+ def on_init_failure(self, target, args, kwargs):
""""""
- def on_load(self, instance):
+ def on_load(self, target):
""""""
- def on_resurrect(self, state, instance):
+ def on_resurrect(self, target):
""""""
+
class MapperEvents(event.Events):
""""""
+ @classmethod
+ def remove(cls, fn, identifier, target):
+ raise NotImplementedError("Removal of mapper events not yet implemented")
class SessionEvents(event.Events):
""""""
-
+ @classmethod
+ def remove(cls, fn, identifier, target):
+ raise NotImplementedError("Removal of session events not yet implemented")
+
class AttributeEvents(event.Events):
- """"""
\ No newline at end of file
+ """Define events for object attributes.
+
+ e.g.::
+
+ from sqlalchemy import event
+ event.listen(my_append_listener, 'on_append', MyClass.collection)
+ event.listen(my_set_listener, 'on_set',
+ MyClass.somescalar, retval=True)
+
+ Several modifiers are available to the listen() function.
+
+ :param active_history=False: When True, indicates that the
+ "on_set" event would like to receive the "old" value
+ being replaced unconditionally, even if this requires
+ firing off database loads.
+ :param propagate=False: When True, the listener function will
+ be established not just for the class attribute given, but
+ for attributes of the same name on all current subclasses
+ of that class, as well as all future subclasses of that
+ class, using an additional listener that listens for
+ instrumentation events.
+ :param raw=False: When True, the "target" argument to the
+ event will be the :class:`.InstanceState` management
+ object, rather than the mapped instance itself.
+ :param retval=False:` when True, the user-defined event
+ listening must return the "value" argument from the
+ function. This gives the listening function the opportunity
+ to change the value that is ultimately used for a "set"
+ or "append" event.
+
+ """
+
+ @classmethod
+ def listen(cls, fn, identifier, target, active_history=False,
+ raw=False, retval=False,
+ propagate=False):
+ if active_history:
+ target.dispatch.active_history = True
+
+ # TODO: for removal, need to package the identity
+ # of the wrapper with the original function.
+
+ if raw is False or retval is False:
+ @util.decorator
+ def wrap(fn, target, value, *arg):
+ if not raw:
+ target = target.obj()
+ if not retval:
+ fn(target, value, *arg)
+ return value
+ else:
+ return fn(target, value, *arg)
+ fn = wrap(fn)
+
+ event.Events.listen(fn, identifier, target)
+
+ if propagate:
+ # TODO: for removal, need to implement
+ # packaging this info for operation in reverse.
+
+ class_ = target.class_
+ for cls in class_.__subclasses__():
+ impl = getattr(cls, target.key)
+ if impl is not target:
+ event.Events.listen(fn, identifier, impl)
+
+ def configure_listener(class_, key, inst):
+ event.Events.listen(fn, identifier, inst)
+ event.listen(configure_listener, 'on_attribute_instrument', class_)
+
+ @classmethod
+ def remove(cls, fn, identifier, target):
+ raise NotImplementedError("Removal of attribute events not yet implemented")
+
+ @classmethod
+ def unwrap(cls, identifier, event):
+ return event['value']
+
+ 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.
+
+ """
+
+@util.decorator
+def _to_instance(fn, state, *arg, **kw):
+ """Marshall the :class:`.InstanceState` argument to an instance."""
+
+ return fn(state.obj(), *arg, **kw)
+
self.manage()
self._instrument_init()
- dispatch = event.dispatcher(events.ClassEvents)
+ dispatch = event.dispatcher(events.InstanceEvents)
@property
def is_mapped(self):
manager.instrument_attribute(key, inst, True)
def post_configure_attribute(self, key):
- pass
+ instrumentation_registry.dispatch.\
+ on_attribute_instrument(self.class_, key, self[key])
def uninstrument_attribute(self, key, propagated=False):
if key not in self:
self._adapted.instrument_attribute(self.class_, key, inst)
def post_configure_attribute(self, key):
+ super(_ClassInstrumentationAdpter, self).post_configure_attribute(key)
self._adapted.post_configure_attribute(self.class_, key, self[key])
def install_descriptor(self, key, inst):
_dict_finders = util.WeakIdentityMapping()
_extended = False
+ dispatch = event.dispatcher(events.InstrumentationEvents)
+
def create_manager_for_cls(self, class_, **kw):
assert class_ is not None
assert manager_of_class(class_) is None
self._manager_finders[class_] = manager.manager_getter()
self._state_finders[class_] = manager.state_getter()
self._dict_finders[class_] = manager.dict_getter()
+
+ self.dispatch.on_class_instrument(class_)
+
return manager
def _collect_management_factories_for(self, cls):
def unregister(self, class_):
if class_ in self._manager_finders:
manager = self.manager_of_class(class_)
+ self.dispatch.on_class_uninstrument(class_)
manager.unregister()
manager.dispose()
del self._manager_finders[class_]
if manager.info.get(_INSTRUMENTOR, False):
return
- event.listen(_event_on_init, 'on_init', manager)
- event.listen(_event_on_init_failure, 'on_init_failure', manager)
- event.listen(_event_on_resurrect, 'on_resurrect', manager)
+ event.listen(_event_on_init, 'on_init', manager, raw=True)
+ event.listen(_event_on_init_failure, 'on_init_failure', manager, raw=True)
+ event.listen(_event_on_resurrect, 'on_resurrect', manager, raw=True)
for key, method in util.iterate_attributes(self.class_):
if isinstance(method, types.FunctionType):
if hasattr(method, '__sa_reconstructor__'):
- event.listen(method, 'on_load', manager)
+ event.listen(method, 'on_load', manager, raw=True)
elif hasattr(method, '__sa_validators__'):
for name in method.__sa_validators__:
self._validators[name] = method
if 'reconstruct_instance' in self.extension:
def reconstruct(instance):
self.extension.reconstruct_instance(self, instance)
- event.listen(reconstruct, 'on_load', manager)
+ event.listen(reconstruct, 'on_load', manager, raw=False)
manager.info[_INSTRUMENTOR] = self
populate_state(state, dict_, row, isnew, attrs)
if loaded_instance:
- state._run_on_load(instance)
+ state._run_on_load()
if result is not None and \
(not append_result or
return fn
return wrap
-def _event_on_init(state, instance, args, kwargs):
+def _event_on_init(state, args, kwargs):
"""Trigger mapper compilation and run init_instance hooks."""
instrumenting_mapper = state.manager.info[_INSTRUMENTOR]
instrumenting_mapper.extension.init_instance(
instrumenting_mapper, instrumenting_mapper.class_,
state.manager.original_init,
- instance, args, kwargs)
+ state.obj(), args, kwargs)
-def _event_on_init_failure(state, instance, args, kwargs):
+def _event_on_init_failure(state, args, kwargs):
"""Run init_failed hooks."""
instrumenting_mapper = state.manager.info[_INSTRUMENTOR]
util.warn_exception(
instrumenting_mapper.extension.init_failed,
instrumenting_mapper, instrumenting_mapper.class_,
- state.manager.original_init, instance, args, kwargs)
+ state.manager.original_init, state.obj(), args, kwargs)
-def _event_on_resurrect(state, instance):
+def _event_on_resurrect(state):
# re-populate the primary key elements
# of the dict based on the mapping.
instrumenting_mapper = state.manager.info[_INSTRUMENTOR]
merged_state.commit_all(merged_dict, self.identity_map)
if new_instance:
- merged_state._run_on_load(merged)
+ merged_state._run_on_load()
return merged
@classmethod
self, instance, args = mixed[0], mixed[1], mixed[2:]
manager = self.manager
- manager.dispatch.on_init(self, instance, args, kwargs)
+ manager.dispatch.on_init(self, args, kwargs)
# LESSTHANIDEAL:
# adjust for the case where the InstanceState was created before
try:
return manager.original_init(*mixed[1:], **kwargs)
except:
- manager.dispatch.on_init_failure(self, instance, args, kwargs)
+ manager.dispatch.on_init_failure(self, args, kwargs)
raise
def get_history(self, key, **kwargs):
else:
return [x]
- def _run_on_load(self, instance):
- self.manager.dispatch.on_load(instance)
+ def _run_on_load(self):
+ self.manager.dispatch.on_load(self)
def __getstate__(self):
d = {'instance':self.obj()}
# re-establishes identity attributes from the key
self.manager.dispatch.on_resurrect(self, obj)
- # TODO: don't really think we should run this here.
- # resurrect is only meant to preserve the minimal state needed to
- # do an UPDATE, not to produce a fully usable object
- self._run_on_load(obj)
-
return obj
class PendingCollection(object):
existing = getattr(self.__target, prop.key)
comparator = existing.comparator.adapted(self.__adapt_element)
- queryattr = attributes.QueryableAttribute(prop.key,
+ queryattr = attributes.QueryableAttribute(self, prop.key,
impl=existing.impl, parententity=self, comparator=comparator)
setattr(self, prop.key, queryattr)
return queryattr
from sqlalchemy.test.schema import Table
from sqlalchemy.test.schema import Column
from sqlalchemy.orm import mapper, relationship, create_session, \
- attributes, class_mapper, clear_mappers, instrumentation
+ attributes, class_mapper, clear_mappers, instrumentation, events
from sqlalchemy.test.testing import eq_, ne_
from sqlalchemy.util import function_named
from test.orm import _base
instrumentation.register_class(cls)
ne_(cls.__init__, original_init)
manager = instrumentation.manager_of_class(cls)
- def on_init(state, instance, args, kwargs):
- canary.append((cls, 'on_init', type(instance)))
- event.listen(on_init, 'on_init', manager)
+ def on_init(state, args, kwargs):
+ canary.append((cls, 'on_init', state.class_))
+ event.listen(on_init, 'on_init', manager, raw=True)
def test_ai(self):
inits = []
@modifies_instrumentation_finders
def test_subclassed(self):
- class MyEvents(instrumentation.ClassEvents):
+ class MyEvents(events.InstanceEvents):
pass
class MyClassManager(instrumentation.ClassManager):
dispatch = event.dispatcher(MyEvents)