diff options
Diffstat (limited to 'lib/sqlalchemy/orm/events.py')
-rw-r--r-- | lib/sqlalchemy/orm/events.py | 311 |
1 files changed, 183 insertions, 128 deletions
diff --git a/lib/sqlalchemy/orm/events.py b/lib/sqlalchemy/orm/events.py index 97019bb4e..a09154dd0 100644 --- a/lib/sqlalchemy/orm/events.py +++ b/lib/sqlalchemy/orm/events.py @@ -1,5 +1,5 @@ # orm/events.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file> +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors <see AUTHORS file> # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -8,10 +8,14 @@ """ from .. import event, exc, util -orm = util.importlater("sqlalchemy", "orm") +from .base import _mapper_or_none import inspect import weakref - +from . import interfaces +from . import mapperlib, instrumentation +from .session import Session, sessionmaker +from .scoping import scoped_session +from .attributes import QueryableAttribute class InstrumentationEvents(event.Events): """Events related to class instrumentation events. @@ -43,17 +47,20 @@ class InstrumentationEvents(event.Events): """ _target_class_doc = "SomeBaseClass" + _dispatch_target = instrumentation.InstrumentationFactory + @classmethod def _accept_with(cls, target): - # TODO: there's no coverage for this if isinstance(target, type): return _InstrumentationEventsHold(target) else: return None @classmethod - def _listen(cls, target, identifier, fn, propagate=True): + def _listen(cls, event_key, propagate=True): + target, identifier, fn = \ + event_key.dispatch_target, event_key.identifier, event_key.fn def listen(target_cls, *arg): listen_cls = target() @@ -63,22 +70,21 @@ class InstrumentationEvents(event.Events): return fn(target_cls, *arg) def remove(ref): - event.Events._remove(orm.instrumentation._instrumentation_factory, - identifier, listen) + key = event.registry._EventKey(None, identifier, listen, + instrumentation._instrumentation_factory) + getattr(instrumentation._instrumentation_factory.dispatch, + identifier).remove(key) target = weakref.ref(target.class_, remove) - event.Events._listen(orm.instrumentation._instrumentation_factory, - identifier, listen) - @classmethod - def _remove(cls, identifier, target, fn): - raise NotImplementedError("Removal of instrumentation events " - "not yet implemented") + event_key.\ + with_dispatch_target(instrumentation._instrumentation_factory).\ + with_wrapper(listen).base_listen() @classmethod def _clear(cls): super(InstrumentationEvents, cls)._clear() - orm.instrumentation._instrumentation_factory.dispatch._clear() + instrumentation._instrumentation_factory.dispatch._clear() def class_instrument(self, cls): """Called after the given class is instrumented. @@ -100,6 +106,7 @@ class InstrumentationEvents(event.Events): """Called when an attribute is instrumented.""" + class _InstrumentationEventsHold(object): """temporary marker object used to transfer from _accept_with() to _listen() on the InstrumentationEvents class. @@ -110,7 +117,6 @@ class _InstrumentationEventsHold(object): dispatch = event.dispatcher(InstrumentationEvents) - class InstanceEvents(event.Events): """Define events specific to object lifecycle. @@ -121,21 +127,19 @@ class InstanceEvents(event.Events): def my_load_listener(target, context): print "on load!" - event.listen(SomeMappedClass, 'load', my_load_listener) - - Available targets include mapped classes, instances of - :class:`.Mapper` (i.e. returned by :func:`.mapper`, - :func:`.class_mapper` and similar), as well as the - :class:`.Mapper` class and :func:`.mapper` function itself - for global event reception:: + event.listen(SomeClass, 'load', my_load_listener) - from sqlalchemy.orm import mapper + Available targets include: - def some_listener(target, context): - log.debug("Instance %s being loaded" % target) + * mapped classes + * unmapped superclasses of mapped or to-be-mapped classes + (using the ``propagate=True`` flag) + * :class:`.Mapper` objects + * the :class:`.Mapper` class itself and the :func:`.mapper` + function indicate listening for all mappers. - # attach to all mappers - event.listen(mapper, 'load', some_listener) + .. versionchanged:: 0.8.0 instance events can be associated with + unmapped superclasses of mapped classes. Instance events are closely related to mapper events, but are more specific to the instance and its instrumentation, @@ -154,21 +158,28 @@ class InstanceEvents(event.Events): """ - _target_class_doc = "SomeMappedClass" + _target_class_doc = "SomeClass" + + _dispatch_target = instrumentation.ClassManager @classmethod - def _accept_with(cls, target): - if isinstance(target, orm.instrumentation.ClassManager): + def _new_classmanager_instance(cls, class_, classmanager): + _InstanceEventsHold.populate(class_, classmanager) + + @classmethod + @util.dependencies("sqlalchemy.orm") + def _accept_with(cls, orm, target): + if isinstance(target, instrumentation.ClassManager): return target - elif isinstance(target, orm.Mapper): + elif isinstance(target, mapperlib.Mapper): return target.class_manager elif target is orm.mapper: - return orm.instrumentation.ClassManager + return instrumentation.ClassManager elif isinstance(target, type): - if issubclass(target, orm.Mapper): - return orm.instrumentation.ClassManager + if issubclass(target, mapperlib.Mapper): + return instrumentation.ClassManager else: - manager = orm.instrumentation.manager_of_class(target) + manager = instrumentation.manager_of_class(target) if manager: return manager else: @@ -176,23 +187,23 @@ class InstanceEvents(event.Events): return None @classmethod - def _listen(cls, target, identifier, fn, raw=False, propagate=False): + def _listen(cls, event_key, raw=False, propagate=False): + target, identifier, fn = \ + event_key.dispatch_target, event_key.identifier, event_key.fn + if not raw: orig_fn = fn def wrap(state, *arg, **kw): return orig_fn(state.obj(), *arg, **kw) fn = wrap + event_key = event_key.with_wrapper(fn) + + event_key.base_listen(propagate=propagate) - event.Events._listen(target, identifier, fn, propagate=propagate) if propagate: for mgr in target.subclass_managers(True): - event.Events._listen(mgr, identifier, fn, True) - - @classmethod - def _remove(cls, identifier, target, fn): - msg = "Removal of instance events not yet implemented" - raise NotImplementedError(msg) + event_key.with_dispatch_target(mgr).base_listen(propagate=True) @classmethod def _clear(cls): @@ -321,8 +332,7 @@ class InstanceEvents(event.Events): """ - -class _EventsHold(object): +class _EventsHold(event.RefCollection): """Hold onto listeners against unmapped, uninstrumented classes. Establish _listen() for that class' mapper/instrumentation when @@ -337,14 +347,20 @@ class _EventsHold(object): cls.all_holds.clear() class HoldEvents(object): + _dispatch_target = None + @classmethod - def _listen(cls, target, identifier, fn, raw=False, propagate=False): + def _listen(cls, event_key, raw=False, propagate=False): + target, identifier, fn = \ + event_key.dispatch_target, event_key.identifier, event_key.fn + if target.class_ in target.all_holds: collection = target.all_holds[target.class_] else: - collection = target.all_holds[target.class_] = [] + collection = target.all_holds[target.class_] = {} - collection.append((identifier, fn, raw, propagate)) + event.registry._stored_in_collection(event_key, target) + collection[event_key._key] = (event_key, raw, propagate) if propagate: stack = list(target.class_.__subclasses__()) @@ -353,28 +369,37 @@ class _EventsHold(object): stack.extend(subclass.__subclasses__()) subject = target.resolve(subclass) if subject is not None: - subject.dispatch._listen(subject, identifier, fn, - raw=raw, propagate=propagate) + event_key.with_dispatch_target(subject).\ + listen(raw=raw, propagate=propagate) + + def remove(self, event_key): + target, identifier, fn = \ + event_key.dispatch_target, event_key.identifier, event_key.fn + + collection = target.all_holds[target.class_] + del collection[event_key._key] @classmethod def populate(cls, class_, subject): for subclass in class_.__mro__: if subclass in cls.all_holds: - if subclass is class_: - collection = cls.all_holds.pop(subclass) - else: - collection = cls.all_holds[subclass] - for ident, fn, raw, propagate in collection: + collection = cls.all_holds[subclass] + for event_key, raw, propagate in collection.values(): if propagate or subclass is class_: - subject.dispatch._listen(subject, ident, - fn, raw, propagate) + # since we can't be sure in what order different classes + # in a hierarchy are triggered with populate(), + # we rely upon _EventsHold for all event + # assignment, instead of using the generic propagate + # flag. + event_key.with_dispatch_target(subject).\ + listen(raw=raw, propagate=False) class _InstanceEventsHold(_EventsHold): all_holds = weakref.WeakKeyDictionary() def resolve(self, class_): - return orm.instrumentation.manager_of_class(class_) + return instrumentation.manager_of_class(class_) class HoldInstanceEvents(_EventsHold.HoldEvents, InstanceEvents): pass @@ -396,24 +421,22 @@ class MapperEvents(event.Events): "select my_special_function(%d)" % target.special_number) - # associate the listener function with SomeMappedClass, + # associate the listener function with SomeClass, # to execute during the "before_insert" hook event.listen( - SomeMappedClass, 'before_insert', my_before_insert_listener) - - Available targets include mapped classes, instances of - :class:`.Mapper` (i.e. returned by :func:`.mapper`, - :func:`.class_mapper` and similar), as well as the - :class:`.Mapper` class and :func:`.mapper` function itself - for global event reception:: + SomeClass, 'before_insert', my_before_insert_listener) - from sqlalchemy.orm import mapper + Available targets include: - def some_listener(mapper, connection, target): - log.debug("Instance %s being inserted" % target) + * mapped classes + * unmapped superclasses of mapped or to-be-mapped classes + (using the ``propagate=True`` flag) + * :class:`.Mapper` objects + * the :class:`.Mapper` class itself and the :func:`.mapper` + function indicate listening for all mappers. - # attach to all mappers - event.listen(mapper, 'before_insert', some_listener) + .. versionchanged:: 0.8.0 mapper events can be associated with + unmapped superclasses of mapped classes. Mapper events provide hooks into critical sections of the mapper, including those related to object instrumentation, @@ -455,17 +478,23 @@ class MapperEvents(event.Events): """ - _target_class_doc = "SomeMappedClass" + _target_class_doc = "SomeClass" + _dispatch_target = mapperlib.Mapper @classmethod - def _accept_with(cls, target): + def _new_mapper_instance(cls, class_, mapper): + _MapperEventsHold.populate(class_, mapper) + + @classmethod + @util.dependencies("sqlalchemy.orm") + def _accept_with(cls, orm, target): if target is orm.mapper: - return orm.Mapper + return mapperlib.Mapper elif isinstance(target, type): - if issubclass(target, orm.Mapper): + if issubclass(target, mapperlib.Mapper): return target else: - mapper = orm.util._mapper_or_none(target) + mapper = _mapper_or_none(target) if mapper is not None: return mapper else: @@ -474,8 +503,10 @@ class MapperEvents(event.Events): return target @classmethod - def _listen(cls, target, identifier, fn, + def _listen(cls, event_key, raw=False, retval=False, propagate=False): + target, identifier, fn = \ + event_key.dispatch_target, event_key.identifier, event_key.fn if not raw or not retval: if not raw: @@ -494,16 +525,17 @@ class MapperEvents(event.Events): arg[target_index] = arg[target_index].obj() if not retval: wrapped_fn(*arg, **kw) - return orm.interfaces.EXT_CONTINUE + return interfaces.EXT_CONTINUE else: return wrapped_fn(*arg, **kw) fn = wrap + event_key = event_key.with_wrapper(wrap) if propagate: for mapper in target.self_and_descendants: - event.Events._listen(mapper, identifier, fn, propagate=True) + event_key.with_dispatch_target(mapper).base_listen(propagate=True) else: - event.Events._listen(target, identifier, fn) + event_key.base_listen() @classmethod def _clear(cls): @@ -517,8 +549,15 @@ class MapperEvents(event.Events): This event is the earliest phase of mapper construction. Most attributes of the mapper are not yet initialized. - This listener can generally only be applied to the :class:`.Mapper` - class overall. + This listener can either be applied to the :class:`.Mapper` + class overall, or to any un-mapped class which serves as a base + for classes that will be mapped (using the ``propagate=True`` flag):: + + Base = declarative_base() + + @event.listens_for(Base, "instrument_class", propagate=True) + def on_new_class(mapper, cls_): + " ... " :param mapper: the :class:`.Mapper` which is the target of this event. @@ -1048,17 +1087,11 @@ class MapperEvents(event.Events): """ - @classmethod - def _remove(cls, identifier, target, fn): - "Removal of mapper events not yet implemented" - raise NotImplementedError(msg) - - class _MapperEventsHold(_EventsHold): all_holds = weakref.WeakKeyDictionary() def resolve(self, class_): - return orm.util._mapper_or_none(class_) + return _mapper_or_none(class_) class HoldMapperEvents(_EventsHold.HoldEvents, MapperEvents): pass @@ -1083,7 +1116,7 @@ class SessionEvents(event.Events): The :func:`~.event.listen` function will accept :class:`.Session` objects as well as the return result - of :func:`.sessionmaker` and :func:`.scoped_session`. + of :class:`~.sessionmaker()` and :class:`~.scoped_session()`. Additionally, it accepts the :class:`.Session` class which will apply listeners to all :class:`.Session` instances @@ -1093,38 +1126,35 @@ class SessionEvents(event.Events): _target_class_doc = "SomeSessionOrFactory" + _dispatch_target = Session + @classmethod def _accept_with(cls, target): - if isinstance(target, orm.scoped_session): + if isinstance(target, scoped_session): target = target.session_factory - if not isinstance(target, orm.sessionmaker) and \ + if not isinstance(target, sessionmaker) and \ ( not isinstance(target, type) or - not issubclass(target, orm.Session) + not issubclass(target, Session) ): raise exc.ArgumentError( "Session event listen on a scoped_session " "requires that its creation callable " "is associated with the Session class.") - if isinstance(target, orm.sessionmaker): + if isinstance(target, sessionmaker): return target.class_ elif isinstance(target, type): - if issubclass(target, orm.scoped_session): - return orm.Session - elif issubclass(target, orm.Session): + if issubclass(target, scoped_session): + return Session + elif issubclass(target, Session): return target - elif isinstance(target, orm.Session): + elif isinstance(target, Session): return target else: return None - @classmethod - def _remove(cls, identifier, target, fn): - msg = "Removal of session events not yet implemented" - raise NotImplementedError(msg) - def after_transaction_create(self, session, transaction): """Execute when a new :class:`.SessionTransaction` is created. @@ -1173,7 +1203,7 @@ class SessionEvents(event.Events): .. note:: - The :meth:`.before_commit` hook is *not* per-flush, + The :meth:`~.SessionEvents.before_commit` hook is *not* per-flush, that is, the :class:`.Session` can emit SQL to the database many times within the scope of a transaction. For interception of these events, use the :meth:`~.SessionEvents.before_flush`, @@ -1265,9 +1295,9 @@ class SessionEvents(event.Events): :param session: The target :class:`.Session`. :param previous_transaction: The :class:`.SessionTransaction` - transactional marker object which was just closed. The current - :class:`.SessionTransaction` for the given :class:`.Session` is - available via the :attr:`.Session.transaction` attribute. + transactional marker object which was just closed. The current + :class:`.SessionTransaction` for the given :class:`.Session` is + available via the :attr:`.Session.transaction` attribute. .. versionadded:: 0.7.3 @@ -1359,7 +1389,7 @@ class SessionEvents(event.Events): This is called before an add, delete or merge causes the object to be part of the session. - .. versionadded:: 0.8. Note that :meth:`.after_attach` now + .. versionadded:: 0.8. Note that :meth:`~.SessionEvents.after_attach` now fires off after the item is part of the session. :meth:`.before_attach` is provided for those cases where the item should not yet be part of the session state. @@ -1474,7 +1504,7 @@ class AttributeEvents(event.Events): listen(UserContact.phone, 'set', validate_phone, retval=True) A validation function like the above can also raise an exception - such as :class:`.ValueError` to halt the operation. + such as :exc:`ValueError` to halt the operation. Several modifiers are available to the :func:`~.event.listen` function. @@ -1503,25 +1533,32 @@ class AttributeEvents(event.Events): """ _target_class_doc = "SomeClass.some_attribute" + _dispatch_target = QueryableAttribute + + @staticmethod + def _set_dispatch(cls, dispatch_cls): + event.Events._set_dispatch(cls, dispatch_cls) + dispatch_cls._active_history = False @classmethod def _accept_with(cls, target): # TODO: coverage - if isinstance(target, orm.interfaces.MapperProperty): + if isinstance(target, interfaces.MapperProperty): return getattr(target.parent.class_, target.key) else: return target @classmethod - def _listen(cls, target, identifier, fn, active_history=False, + def _listen(cls, event_key, active_history=False, raw=False, retval=False, propagate=False): + + target, identifier, fn = \ + event_key.dispatch_target, event_key.identifier, event_key.fn + if active_history: target.dispatch._active_history = True - # TODO: for removal, need to package the identity - # of the wrapper with the original function. - if not raw or not retval: orig_fn = fn @@ -1534,19 +1571,15 @@ class AttributeEvents(event.Events): else: return orig_fn(target, value, *arg) fn = wrap + event_key = event_key.with_wrapper(wrap) - event.Events._listen(target, identifier, fn, propagate) + event_key.base_listen(propagate=propagate) if propagate: - manager = orm.instrumentation.manager_of_class(target.class_) + manager = instrumentation.manager_of_class(target.class_) for mgr in manager.subclass_managers(True): - event.Events._listen(mgr[target.key], identifier, fn, True) - - @classmethod - def _remove(cls, identifier, target, fn): - msg = "Removal of attribute events not yet implemented" - raise NotImplementedError(msg) + event_key.with_dispatch_target(mgr[target.key]).base_listen(propagate=True) def append(self, target, value, initiator): """Receive a collection append event. @@ -1558,8 +1591,15 @@ class AttributeEvents(event.Events): is registered with ``retval=True``, the listener function must return this value, or a new value which replaces it. - :param initiator: the attribute implementation object - which initiated this event. + :param initiator: An instance of :class:`.attributes.Event` + representing the initiation of the event. May be modified + from it's original value by backref handlers in order to control + chained event propagation. + + .. versionchanged:: 0.9.0 the ``initiator`` argument is now + passed as a :class:`.attributes.Event` object, and may be modified + by backref handlers within a chain of backref-linked events. + :return: if the event was registered with ``retval=True``, the given value, or a new effective value, should be returned. @@ -1572,8 +1612,15 @@ class AttributeEvents(event.Events): If the listener is registered with ``raw=True``, this will be the :class:`.InstanceState` object. :param value: the value being removed. - :param initiator: the attribute implementation object - which initiated this event. + :param initiator: An instance of :class:`.attributes.Event` + representing the initiation of the event. May be modified + from it's original value by backref handlers in order to control + chained event propagation. + + .. versionchanged:: 0.9.0 the ``initiator`` argument is now + passed as a :class:`.attributes.Event` object, and may be modified + by backref handlers within a chain of backref-linked events. + :return: No return value is defined for this event. """ @@ -1593,9 +1640,17 @@ class AttributeEvents(event.Events): the previous value of the attribute will be loaded from the database if the existing value is currently unloaded or expired. - :param initiator: the attribute implementation object - which initiated this event. + :param initiator: An instance of :class:`.attributes.Event` + representing the initiation of the event. May be modified + from it's original value by backref handlers in order to control + chained event propagation. + + .. versionchanged:: 0.9.0 the ``initiator`` argument is now + passed as a :class:`.attributes.Event` object, and may be modified + by backref handlers within a chain of backref-linked events. + :return: if the event was registered with ``retval=True``, the given value, or a new effective value, should be returned. """ + |