From: Mike Bayer Date: Thu, 18 Nov 2010 23:49:42 +0000 (-0500) Subject: merge tip X-Git-Tag: rel_0_7b1~253^2~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=09a82f90e63ec6eeaaa5610b0f495fb2b5a10fce;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git merge tip --- 09a82f90e63ec6eeaaa5610b0f495fb2b5a10fce diff --cc lib/sqlalchemy/orm/deprecated_interfaces.py index 3e4f268d08,0000000000..52193b149a mode 100644,000000..100644 --- a/lib/sqlalchemy/orm/deprecated_interfaces.py +++ b/lib/sqlalchemy/orm/deprecated_interfaces.py @@@ -1,569 -1,0 +1,573 @@@ +from sqlalchemy import event, util +from interfaces import EXT_CONTINUE + + +class MapperExtension(object): + """Base implementation for :class:`.Mapper` event hooks. + + .. note:: :class:`.MapperExtension` is deprecated. Please + refer to :func:`.event.listen` as well as + :class:`.MapperEvents`. + + New extension classes subclass :class:`.MapperExtension` and are specified + using the ``extension`` mapper() argument, which is a single + :class:`.MapperExtension` or a list of such:: + + from sqlalchemy.orm.interfaces import MapperExtension + + class MyExtension(MapperExtension): + def before_insert(self, mapper, connection, instance): + print "instance %s before insert !" % instance + + m = mapper(User, users_table, extension=MyExtension()) + + A single mapper can maintain a chain of ``MapperExtension`` + objects. When a particular mapping event occurs, the + corresponding method on each ``MapperExtension`` is invoked + serially, and each method has the ability to halt the chain + from proceeding further:: + + m = mapper(User, users_table, extension=[ext1, ext2, ext3]) + + Each ``MapperExtension`` method returns the symbol + EXT_CONTINUE by default. This symbol generally means "move + to the next ``MapperExtension`` for processing". For methods + that return objects like translated rows or new object + instances, EXT_CONTINUE means the result of the method + should be ignored. In some cases it's required for a + default mapper activity to be performed, such as adding a + new instance to a result list. + + The symbol EXT_STOP has significance within a chain + of ``MapperExtension`` objects that the chain will be stopped + when this symbol is returned. Like EXT_CONTINUE, it also + has additional significance in some cases that a default + mapper activity will not be performed. + + """ + + @classmethod + def _adapt_instrument_class(cls, self, listener): + cls._adapt_listener_methods(self, listener, ('instrument_class',)) + + @classmethod + def _adapt_listener(cls, self, listener): + cls._adapt_listener_methods( + self, listener, + ( + 'init_instance', + 'init_failed', + 'translate_row', + 'create_instance', + 'append_result', + 'populate_instance', + 'reconstruct_instance', + 'before_insert', + 'after_insert', + 'before_update', + 'after_update', + 'before_delete', + 'after_delete' + )) + + @classmethod + def _adapt_listener_methods(cls, self, listener, methods): + + for meth in methods: + me_meth = getattr(MapperExtension, meth) + ls_meth = getattr(listener, meth) + + # TODO: comparing self.methods to cls.method, + # this comparison is probably moot + + if me_meth is not ls_meth: + if meth == 'reconstruct_instance': + def go(ls_meth): + def reconstruct(instance): + ls_meth(self, instance) + return reconstruct + event.listen(go(ls_meth), 'on_load', + self.class_manager, raw=False, propagate=True) + elif meth == 'init_instance': + def go(ls_meth): + def init_instance(instance, args, kwargs): + ls_meth(self, self.class_, + self.class_manager.original_init, + instance, args, kwargs) + return init_instance + event.listen(go(ls_meth), 'on_init', + self.class_manager, raw=False, propagate=True) + elif meth == 'init_failed': + def go(ls_meth): + def init_failed(instance, args, kwargs): + util.warn_exception(ls_meth, self, self.class_, + self.class_manager.original_init, + instance, args, kwargs) + + return init_failed + event.listen(go(ls_meth), 'on_init_failure', + self.class_manager, raw=False, propagate=True) + else: + event.listen(ls_meth, "on_%s" % meth, self, + raw=False, retval=True, propagate=True) + + + def instrument_class(self, mapper, class_): + """Receive a class when the mapper is first constructed, and has + applied instrumentation to the mapped class. + + The return value is only significant within the ``MapperExtension`` + chain; the parent mapper's behavior isn't modified by this method. + + """ + return EXT_CONTINUE + + def init_instance(self, mapper, class_, oldinit, instance, args, kwargs): + """Receive an instance when it's constructor is called. + + This method is only called during a userland construction of + an object. It is not called when an object is loaded from the + database. + + The return value is only significant within the ``MapperExtension`` + chain; the parent mapper's behavior isn't modified by this method. + + """ + return EXT_CONTINUE + + def init_failed(self, mapper, class_, oldinit, instance, args, kwargs): + """Receive an instance when it's constructor has been called, + and raised an exception. + + This method is only called during a userland construction of + an object. It is not called when an object is loaded from the + database. + + The return value is only significant within the ``MapperExtension`` + chain; the parent mapper's behavior isn't modified by this method. + + """ + return EXT_CONTINUE + + def translate_row(self, mapper, context, row): + """Perform pre-processing on the given result row and return a + new row instance. + + This is called when the mapper first receives a row, before + the object identity or the instance itself has been derived + from that row. The given row may or may not be a + ``RowProxy`` object - it will always be a dictionary-like + object which contains mapped columns as keys. The + returned object should also be a dictionary-like object + which recognizes mapped columns as keys. + + If the ultimate return value is EXT_CONTINUE, the row + is not translated. + + """ + return EXT_CONTINUE + + def create_instance(self, mapper, selectcontext, row, class_): + """Receive a row when a new object instance is about to be + created from that row. + + The method can choose to create the instance itself, or it can return + EXT_CONTINUE to indicate normal object creation should take place. + + mapper + The mapper doing the operation + + selectcontext + The QueryContext generated from the Query. + + row + The result row from the database + + class\_ + The class we are mapping. + + return value + A new object instance, or EXT_CONTINUE + + """ + return EXT_CONTINUE + + def append_result(self, mapper, selectcontext, row, instance, + result, **flags): + """Receive an object instance before that instance is appended + to a result list. + + If this method returns EXT_CONTINUE, result appending will proceed + normally. if this method returns any other value or None, + result appending will not proceed for this instance, giving + this extension an opportunity to do the appending itself, if + desired. + + mapper + The mapper doing the operation. + + selectcontext + The QueryContext generated from the Query. + + row + The result row from the database. + + instance + The object instance to be appended to the result. + + result + List to which results are being appended. + + \**flags + extra information about the row, same as criterion in + ``create_row_processor()`` method of + :class:`~sqlalchemy.orm.interfaces.MapperProperty` + """ + + return EXT_CONTINUE + + def populate_instance(self, mapper, selectcontext, row, + instance, **flags): + """Receive an instance before that instance has + its attributes populated. + + This usually corresponds to a newly loaded instance but may + also correspond to an already-loaded instance which has + unloaded attributes to be populated. The method may be called + many times for a single instance, as multiple result rows are + used to populate eagerly loaded collections. + + If this method returns EXT_CONTINUE, instance population will + proceed normally. If any other value or None is returned, + instance population will not proceed, giving this extension an + opportunity to populate the instance itself, if desired. + + As of 0.5, most usages of this hook are obsolete. For a + generic "object has been newly created from a row" hook, use + ``reconstruct_instance()``, or the ``@orm.reconstructor`` + decorator. + + """ + return EXT_CONTINUE + + def reconstruct_instance(self, mapper, instance): + """Receive an object instance after it has been created via + ``__new__``, and after initial attribute population has + occurred. + + This typically occurs when the instance is created based on + incoming result rows, and is only called once for that + instance's lifetime. + + Note that during a result-row load, this method is called upon + the first row received for this instance. Note that some + attributes and collections may or may not be loaded or even + initialized, depending on what's present in the result rows. + + The return value is only significant within the ``MapperExtension`` + chain; the parent mapper's behavior isn't modified by this method. + + """ + return EXT_CONTINUE + + def before_insert(self, mapper, connection, instance): + """Receive an object instance before that instance is inserted + into its table. + + This is a good place to set up primary key values and such + that aren't handled otherwise. + + Column-based attributes can be modified within this method + which will result in the new value being inserted. However + *no* changes to the overall flush plan can be made, and + manipulation of the ``Session`` will not have the desired effect. + To manipulate the ``Session`` within an extension, use + ``SessionExtension``. + + The return value is only significant within the ``MapperExtension`` + chain; the parent mapper's behavior isn't modified by this method. + + """ + + return EXT_CONTINUE + + def after_insert(self, mapper, connection, instance): + """Receive an object instance after that instance is inserted. + + The return value is only significant within the ``MapperExtension`` + chain; the parent mapper's behavior isn't modified by this method. + + """ + + return EXT_CONTINUE + + def before_update(self, mapper, connection, instance): + """Receive an object instance before that instance is updated. + + Note that this method is called for all instances that are marked as + "dirty", even those which have no net changes to their column-based + attributes. An object is marked as dirty when any of its column-based + attributes have a "set attribute" operation called or when any of its + collections are modified. If, at update time, no column-based + attributes have any net changes, no UPDATE statement will be issued. + This means that an instance being sent to before_update is *not* a + guarantee that an UPDATE statement will be issued (although you can + affect the outcome here). + + To detect if the column-based attributes on the object have net + changes, and will therefore generate an UPDATE statement, use + ``object_session(instance).is_modified(instance, + include_collections=False)``. + + Column-based attributes can be modified within this method + which will result in the new value being updated. However + *no* changes to the overall flush plan can be made, and + manipulation of the ``Session`` will not have the desired effect. + To manipulate the ``Session`` within an extension, use + ``SessionExtension``. + + The return value is only significant within the ``MapperExtension`` + chain; the parent mapper's behavior isn't modified by this method. + + """ + + return EXT_CONTINUE + + def after_update(self, mapper, connection, instance): + """Receive an object instance after that instance is updated. + + The return value is only significant within the ``MapperExtension`` + chain; the parent mapper's behavior isn't modified by this method. + + """ + + return EXT_CONTINUE + + def before_delete(self, mapper, connection, instance): + """Receive an object instance before that instance is deleted. + + Note that *no* changes to the overall flush plan can be made + here; and manipulation of the ``Session`` will not have the + desired effect. To manipulate the ``Session`` within an + extension, use ``SessionExtension``. + + The return value is only significant within the ``MapperExtension`` + chain; the parent mapper's behavior isn't modified by this method. + + """ + + return EXT_CONTINUE + + def after_delete(self, mapper, connection, instance): + """Receive an object instance after that instance is deleted. + + The return value is only significant within the ``MapperExtension`` + chain; the parent mapper's behavior isn't modified by this method. + + """ + + return EXT_CONTINUE + +class SessionExtension(object): + + """Base implementation for :class:`.Session` event hooks. + + .. note:: :class:`.SessionExtension` is deprecated. Please + refer to :func:`.event.listen` as well as + :class:`.SessionEvents`. + + Subclasses may be installed into a :class:`.Session` (or + :func:`.sessionmaker`) using the ``extension`` keyword + argument:: + + from sqlalchemy.orm.interfaces import SessionExtension + + class MySessionExtension(SessionExtension): + def before_commit(self, session): + print "before commit!" + + Session = sessionmaker(extension=MySessionExtension()) + + The same :class:`.SessionExtension` instance can be used + with any number of sessions. + + """ + + @classmethod + def _adapt_listener(cls, self, listener): + event.listen(listener.before_commit, 'on_before_commit', self) + event.listen(listener.after_commit, 'on_after_commit', self) + event.listen(listener.after_rollback, 'on_after_rollback', self) + event.listen(listener.before_flush, 'on_before_flush', self) + event.listen(listener.after_flush, 'on_after_flush', self) + event.listen(listener.after_flush_postexec, 'on_after_flush_postexec', self) + event.listen(listener.after_begin, 'on_after_begin', self) + event.listen(listener.after_attach, 'on_after_attach', self) + event.listen(listener.after_bulk_update, 'on_after_bulk_update', self) + event.listen(listener.after_bulk_delete, 'on_after_bulk_delete', self) + + def before_commit(self, session): + """Execute right before commit is called. + + Note that this may not be per-flush if a longer running + transaction is ongoing.""" + + def after_commit(self, session): + """Execute after a commit has occured. + + Note that this may not be per-flush if a longer running + transaction is ongoing.""" + + def after_rollback(self, session): + """Execute after a rollback has occured. + + Note that this may not be per-flush if a longer running + transaction is ongoing.""" + + def before_flush( self, session, flush_context, instances): + """Execute before flush process has started. + + `instances` is an optional list of objects which were passed to + the ``flush()`` method. """ + + def after_flush(self, session, flush_context): + """Execute after flush has completed, but before commit has been + called. + + Note that the session's state is still in pre-flush, i.e. 'new', + 'dirty', and 'deleted' lists still show pre-flush state as well + as the history settings on instance attributes.""" + + def after_flush_postexec(self, session, flush_context): + """Execute after flush has completed, and after the post-exec + state occurs. + + This will be when the 'new', 'dirty', and 'deleted' lists are in + their final state. An actual commit() may or may not have + occured, depending on whether or not the flush started its own + transaction or participated in a larger transaction. """ + + def after_begin( self, session, transaction, connection): + """Execute after a transaction is begun on a connection + + `transaction` is the SessionTransaction. This method is called + after an engine level transaction is begun on a connection. """ + + def after_attach(self, session, instance): + """Execute after an instance is attached to a session. + + This is called after an add, delete or merge. """ + + def after_bulk_update( self, session, query, query_context, result): + """Execute after a bulk update operation to the session. + + This is called after a session.query(...).update() + + `query` is the query object that this update operation was + called on. `query_context` was the query context object. + `result` is the result object returned from the bulk operation. + """ + + def after_bulk_delete( self, session, query, query_context, result): + """Execute after a bulk delete operation to the session. + + This is called after a session.query(...).delete() + + `query` is the query object that this delete operation was + called on. `query_context` was the query context object. + `result` is the result object returned from the bulk operation. + """ + + +class AttributeExtension(object): + """Base implementation for :class:`.AttributeImpl` event hooks, events + that fire upon attribute mutations in user code. + + .. note:: :class:`.AttributeExtension` is deprecated. Please + refer to :func:`.event.listen` as well as + :class:`.AttributeEvents`. + + :class:`.AttributeExtension` is used to listen for set, + remove, and append events on individual mapped attributes. + It is established on an individual mapped attribute using + the `extension` argument, available on + :func:`.column_property`, :func:`.relationship`, and + others:: + + from sqlalchemy.orm.interfaces import AttributeExtension + from sqlalchemy.orm import mapper, relationship, column_property + + class MyAttrExt(AttributeExtension): + def append(self, state, value, initiator): + print "append event !" + return value + + def set(self, state, value, oldvalue, initiator): + print "set event !" + return value + + mapper(SomeClass, sometable, properties={ + 'foo':column_property(sometable.c.foo, extension=MyAttrExt()), + 'bar':relationship(Bar, extension=MyAttrExt()) + }) + + Note that the :class:`AttributeExtension` methods + :meth:`~.AttributeExtension.append` and + :meth:`~.AttributeExtension.set` need to return the + ``value`` parameter. The returned value is used as the + effective value, and allows the extension to change what is + ultimately persisted. + + AttributeExtension is assembled within the descriptors associated + with a mapped class. + + """ + + active_history = True + """indicates that the set() method would like to receive the 'old' value, + even if it means firing lazy callables. ++ ++ Note that ``active_history`` can also be set directly via ++ :func:`.column_property` and :func:`.relationship`. ++ + """ + + @classmethod + def _adapt_listener(cls, self, listener): + event.listen(listener.append, 'on_append', self, + active_history=listener.active_history, + raw=True, retval=True) + event.listen(listener.remove, 'on_remove', self, + active_history=listener.active_history, + raw=True, retval=True) + event.listen(listener.set, 'on_set', self, + active_history=listener.active_history, + raw=True, retval=True) + + def append(self, state, value, initiator): + """Receive a collection append event. + + The returned value will be used as the actual value to be + appended. + + """ + return value + + def remove(self, state, value, initiator): + """Receive a remove event. + + No return value is defined. + + """ + pass + + def set(self, state, value, oldvalue, initiator): + """Receive a set event. + + The returned value will be used as the actual value to be + set. + + """ + return value + + diff --cc lib/sqlalchemy/orm/events.py index fc8cab2ed7,0000000000..b610408b74 mode 100644,000000..100644 --- a/lib/sqlalchemy/orm/events.py +++ b/lib/sqlalchemy/orm/events.py @@@ -1,893 -1,0 +1,896 @@@ +"""ORM event interfaces. + +""" +from sqlalchemy import event, exc +import inspect + +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 instrumentation_registry + + if isinstance(target, type): + return instrumentation_registry + else: + return None + + @classmethod + def listen(cls, fn, identifier, target, propagate=False): + event.Events.listen(fn, identifier, target, propagate=propagate) + + @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): + """Define events specific to object lifecycle. + + Instance-level don't automatically propagate their associations + to subclasses. + + """ + @classmethod + def accept_with(cls, target): + from sqlalchemy.orm.instrumentation import ClassManager, manager_of_class + from sqlalchemy.orm import Mapper, mapper + + if isinstance(target, ClassManager): + return target + elif isinstance(target, Mapper): + return target.class_manager + elif target is mapper: + return ClassManager + elif isinstance(target, type): + if issubclass(target, Mapper): + return ClassManager + else: + manager = manager_of_class(target) + if manager: + return manager + return None + + @classmethod + def listen(cls, fn, identifier, target, raw=False, propagate=False): + if not raw: + orig_fn = fn + def wrap(state, *arg, **kw): + return orig_fn(state.obj(), *arg, **kw) + fn = wrap + + event.Events.listen(fn, identifier, target, propagate=propagate) + if propagate: + for mgr in target.subclass_managers(True): + event.Events.listen(fn, identifier, mgr, True) + + @classmethod + def remove(cls, fn, identifier, target): + raise NotImplementedError("Removal of instance events not yet implemented") + + def on_init(self, target, args, kwargs): + """Receive an instance when it's constructor is called. + + This method is only called during a userland construction of + an object. It is not called when an object is loaded from the + database. + + """ + + def on_init_failure(self, target, args, kwargs): + """Receive an instance when it's constructor has been called, + and raised an exception. + + This method is only called during a userland construction of + an object. It is not called when an object is loaded from the + database. + + """ + + def on_load(self, target): + """Receive an object instance after it has been created via + ``__new__``, and after initial attribute population has + occurred. + + This typically occurs when the instance is created based on + incoming result rows, and is only called once for that + instance's lifetime. + + Note that during a result-row load, this method is called upon + the first row received for this instance. Note that some + attributes and collections may or may not be loaded or even + initialized, depending on what's present in the result rows. + + """ + + def on_resurrect(self, target): + """Receive an object instance as it is 'resurrected' from + garbage collection, which occurs when a "dirty" state falls + out of scope.""" + + +class MapperEvents(event.Events): + """Define events specific to mappings. + + e.g.:: + + from sqlalchemy import event + + def my_before_insert_listener(mapper, connection, target): + # execute a stored procedure upon INSERT, + # apply the value to the row to be inserted + target.calculated_value = connection.scalar( + "select my_special_function(%d)" + % target.special_number) + + # associate the listener function with SomeMappedClass, + # to execute during the "on_before_insert" hook + event.listen(my_before_insert_listener, 'on_before_insert', SomeMappedClass) + + 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:: + + from sqlalchemy.orm import mapper + + def some_listener(mapper, connection, target): + log.debug("Instance %s being inserted" % target) + + # attach to all mappers + event.listen(some_listener, 'on_before_insert', mapper) + + Mapper events provide hooks into critical sections of the + mapper, including those related to object instrumentation, + object loading, and object persistence. In particular, the + persistence methods :meth:`~.MapperEvents.on_before_insert`, + and :meth:`~.MapperEvents.on_before_update` are popular + places to augment the state being persisted - however, these + methods operate with several significant restrictions. The + user is encouraged to evaluate the + :meth:`.SessionEvents.on_before_flush` and + :meth:`.SessionEvents.on_after_flush` methods as more + flexible and user-friendly hooks in which to apply + additional database state during a flush. + + When using :class:`.MapperEvents`, several modifiers are + available to the :func:`.event.listen` function. + + :param propagate=False: When True, the event listener should + be applied to all inheriting mappers as well as the + mapper which is the target of this listener. + :param raw=False: When True, the "target" argument passed + to applicable event listener functions will be the + instance's :class:`.InstanceState` management + object, rather than the mapped instance itself. + :param retval=False: when True, the user-defined event function + must have a return value, the purpose of which is either to + control subsequent event propagation, or to otherwise alter + the operation in progress by the mapper. Possible return + values are: + + * ``sqlalchemy.orm.interfaces.EXT_CONTINUE`` - continue event + processing normally. + * ``sqlalchemy.orm.interfaces.EXT_STOP`` - cancel all subsequent + event handlers in the chain. + * other values - the return value specified by specific listeners, + such as :meth:`~.MapperEvents.on_translate_row` or + :meth:`~.MapperEvents.on_create_instance`. + + """ + + @classmethod + def accept_with(cls, target): + from sqlalchemy.orm import mapper, class_mapper, Mapper + if target is mapper: + return Mapper + elif isinstance(target, type): + if issubclass(target, Mapper): + return target + else: + return class_mapper(target) + else: + return target + + @classmethod + def listen(cls, fn, identifier, target, + raw=False, retval=False, propagate=False): + from sqlalchemy.orm.interfaces import EXT_CONTINUE + + if not raw or not retval: + if not raw: + meth = getattr(cls, identifier) + try: + target_index = inspect.getargspec(meth)[0].index('target') - 1 + except ValueError: + target_index = None + + wrapped_fn = fn + def wrap(*arg, **kw): + if not raw and target_index is not None: + arg = list(arg) + arg[target_index] = arg[target_index].obj() + if not retval: + wrapped_fn(*arg, **kw) + return EXT_CONTINUE + else: + return wrapped_fn(*arg, **kw) + fn = wrap + + if propagate: + for mapper in target.self_and_descendants: + event.Events.listen(fn, identifier, mapper, propagate=True) + else: + event.Events.listen(fn, identifier, target) + + def on_instrument_class(self, mapper, class_): + """Receive a class when the mapper is first constructed, and has + applied instrumentation to the mapped class. + + This listener can generally only be applied to the :class:`.Mapper` + class overall. + + :param mapper: the :class:`.Mapper` which is the target + of this event. + :param class\_: the mapped class. + + """ + + def on_translate_row(self, mapper, context, row): + """Perform pre-processing on the given result row and return a + new row instance. + + This listener is typically registered with ``retval=True``. + It is called when the mapper first receives a row, before + the object identity or the instance itself has been derived + from that row. The given row may or may not be a + :class:`.RowProxy` object - it will always be a dictionary-like + object which contains mapped columns as keys. The + returned object should also be a dictionary-like object + which recognizes mapped columns as keys. + + :param mapper: the :class:`.Mapper` which is the target + of this event. + :param context: the :class:`.QueryContext`, which includes + a handle to the current :class:`.Query` in progress as well + as additional state information. + :param row: the result row being handled. This may be + an actual :class:`.RowProxy` or may be a dictionary containing + :class:`.Column` objects as keys. + :return: When configured with ``retval=True``, the function + should return a dictionary-like row object, or ``EXT_CONTINUE``, + indicating the original row should be used. + + + """ + + def on_create_instance(self, mapper, context, row, class_): + """Receive a row when a new object instance is about to be + created from that row. + + The method can choose to create the instance itself, or it can return + EXT_CONTINUE to indicate normal object creation should take place. + This listener is typically registered with ``retval=True``. + + :param mapper: the :class:`.Mapper` which is the target + of this event. + :param context: the :class:`.QueryContext`, which includes + a handle to the current :class:`.Query` in progress as well + as additional state information. + :param row: the result row being handled. This may be + an actual :class:`.RowProxy` or may be a dictionary containing + :class:`.Column` objects as keys. + :param class\_: the mapped class. + :return: When configured with ``retval=True``, the return value + should be a newly created instance of the mapped class, + or ``EXT_CONTINUE`` indicating that default object construction + should take place. + + """ + + def on_append_result(self, mapper, context, row, target, + result, **flags): + """Receive an object instance before that instance is appended + to a result list. + + This is a rarely used hook which can be used to alter + the construction of a result list returned by :class:`.Query`. + + :param mapper: the :class:`.Mapper` which is the target + of this event. + :param context: the :class:`.QueryContext`, which includes + a handle to the current :class:`.Query` in progress as well + as additional state information. + :param row: the result row being handled. This may be + an actual :class:`.RowProxy` or may be a dictionary containing + :class:`.Column` objects as keys. + :param target: the mapped instance being populated. If + the event is configured with ``raw=True``, this will + instead be the :class:`.InstanceState` state-management + object associated with the instance. + :param result: a list-like object where results are being + appended. + :param \**flags: Additional state information about the + current handling of the row. + :return: If this method is registered with ``retval=True``, + a return value of ``EXT_STOP`` will prevent the instance + from being appended to the given result list, whereas a + return value of ``EXT_CONTINUE`` will result in the default + behavior of appending the value to the result list. + + """ + + + def on_populate_instance(self, mapper, context, row, + target, **flags): + """Receive an instance before that instance has + its attributes populated. + + This usually corresponds to a newly loaded instance but may + also correspond to an already-loaded instance which has + unloaded attributes to be populated. The method may be called + many times for a single instance, as multiple result rows are + used to populate eagerly loaded collections. + + Most usages of this hook are obsolete. For a + generic "object has been newly created from a row" hook, use + :meth:`.InstanceEvents.on_load`. + + :param mapper: the :class:`.Mapper` which is the target + of this event. + :param context: the :class:`.QueryContext`, which includes + a handle to the current :class:`.Query` in progress as well + as additional state information. + :param row: the result row being handled. This may be + an actual :class:`.RowProxy` or may be a dictionary containing + :class:`.Column` objects as keys. + :param class\_: the mapped class. + :return: When configured with ``retval=True``, a return + value of ``EXT_STOP`` will bypass instance population by + the mapper. A value of ``EXT_CONTINUE`` indicates that + default instance population should take place. + + """ + + def on_before_insert(self, mapper, connection, target): + """Receive an object instance before an INSERT statement + is emitted corresponding to that instance. + + This event is used to modify local, non-object related + attributes on the instance before an INSERT occurs, as well + as to emit additional SQL statements on the given + connection. + + The event is often called for a batch of objects of the + same class before their INSERT statements are emitted at + once in a later step. In the extremely rare case that + this is not desirable, the :func:`.mapper` can be + configured with ``batch=False``, which will cause + batches of instances to be broken up into individual + (and more poorly performing) event->persist->event + steps. + + Handlers should **not** modify any attributes which are + mapped by :func:`.relationship`, nor should they attempt + to make any modifications to the :class:`.Session` in + this hook (including :meth:`.Session.add`, + :meth:`.Session.delete`, etc.) - such changes will not + take effect. For overall changes to the "flush plan", + use :meth:`.SessionEvents.before_flush`. + + :param mapper: the :class:`.Mapper` which is the target + of this event. + :param connection: the :class:`.Connection` being used to + emit INSERT statements for this instance. This + provides a handle into the current transaction on the + target database specific to this instance. + :param target: the mapped instance being persisted. If + the event is configured with ``raw=True``, this will + instead be the :class:`.InstanceState` state-management + object associated with the instance. + :return: No return value is supported by this event. + + """ + + def on_after_insert(self, mapper, connection, target): + """Receive an object instance after an INSERT statement + is emitted corresponding to that instance. + + This event is used to modify in-Python-only + state on the instance after an INSERT occurs, as well + as to emit additional SQL statements on the given + connection. + + The event is often called for a batch of objects of the + same class after their INSERT statements have been + emitted at once in a previous step. In the extremely + rare case that this is not desirable, the + :func:`.mapper` can be configured with ``batch=False``, + which will cause batches of instances to be broken up + into individual (and more poorly performing) + event->persist->event steps. + + :param mapper: the :class:`.Mapper` which is the target + of this event. + :param connection: the :class:`.Connection` being used to + emit INSERT statements for this instance. This + provides a handle into the current transaction on the + target database specific to this instance. + :param target: the mapped instance being persisted. If + the event is configured with ``raw=True``, this will + instead be the :class:`.InstanceState` state-management + object associated with the instance. + :return: No return value is supported by this event. + + """ + + def on_before_update(self, mapper, connection, target): + """Receive an object instance before an UPDATE statement + is emitted corresponding to that instance. + + This event is used to modify local, non-object related + attributes on the instance before an UPDATE occurs, as well + as to emit additional SQL statements on the given + connection. + + This method is called for all instances that are + marked as "dirty", *even those which have no net changes + to their column-based attributes*. An object is marked + as dirty when any of its column-based attributes have a + "set attribute" operation called or when any of its + collections are modified. If, at update time, no + column-based attributes have any net changes, no UPDATE + statement will be issued. This means that an instance + being sent to :meth:`~.MapperEvents.on_before_update` is + *not* a guarantee that an UPDATE statement will be + issued, although you can affect the outcome here by + modifying attributes so that a net change in value does + exist. + + To detect if the column-based attributes on the object have net + changes, and will therefore generate an UPDATE statement, use + ``object_session(instance).is_modified(instance, + include_collections=False)``. + + The event is often called for a batch of objects of the + same class before their UPDATE statements are emitted at + once in a later step. In the extremely rare case that + this is not desirable, the :func:`.mapper` can be + configured with ``batch=False``, which will cause + batches of instances to be broken up into individual + (and more poorly performing) event->persist->event + steps. + + Handlers should **not** modify any attributes which are + mapped by :func:`.relationship`, nor should they attempt + to make any modifications to the :class:`.Session` in + this hook (including :meth:`.Session.add`, + :meth:`.Session.delete`, etc.) - such changes will not + take effect. For overall changes to the "flush plan", + use :meth:`.SessionEvents.before_flush`. + + :param mapper: the :class:`.Mapper` which is the target + of this event. + :param connection: the :class:`.Connection` being used to + emit UPDATE statements for this instance. This + provides a handle into the current transaction on the + target database specific to this instance. + :param target: the mapped instance being persisted. If + the event is configured with ``raw=True``, this will + instead be the :class:`.InstanceState` state-management + object associated with the instance. + :return: No return value is supported by this event. + """ + + def on_after_update(self, mapper, connection, target): + """Receive an object instance after an UPDATE statement + is emitted corresponding to that instance. + + This event is used to modify in-Python-only + state on the instance after an UPDATE occurs, as well + as to emit additional SQL statements on the given + connection. + + This method is called for all instances that are + marked as "dirty", *even those which have no net changes + to their column-based attributes*, and for which + no UPDATE statement has proceeded. An object is marked + as dirty when any of its column-based attributes have a + "set attribute" operation called or when any of its + collections are modified. If, at update time, no + column-based attributes have any net changes, no UPDATE + statement will be issued. This means that an instance + being sent to :meth:`~.MapperEvents.on_after_update` is + *not* a guarantee that an UPDATE statement has been + issued. + + To detect if the column-based attributes on the object have net + changes, and therefore resulted in an UPDATE statement, use + ``object_session(instance).is_modified(instance, + include_collections=False)``. + + The event is often called for a batch of objects of the + same class after their UPDATE statements have been emitted at + once in a previous step. In the extremely rare case that + this is not desirable, the :func:`.mapper` can be + configured with ``batch=False``, which will cause + batches of instances to be broken up into individual + (and more poorly performing) event->persist->event + steps. + + :param mapper: the :class:`.Mapper` which is the target + of this event. + :param connection: the :class:`.Connection` being used to + emit UPDATE statements for this instance. This + provides a handle into the current transaction on the + target database specific to this instance. + :param target: the mapped instance being persisted. If + the event is configured with ``raw=True``, this will + instead be the :class:`.InstanceState` state-management + object associated with the instance. + :return: No return value is supported by this event. + + """ + + def on_before_delete(self, mapper, connection, target): + """Receive an object instance before a DELETE statement + is emitted corresponding to that instance. + + This event is used to emit additional SQL statements on + the given connection as well as to perform application + specific bookkeeping related to a deletion event. + + The event is often called for a batch of objects of the + same class before their DELETE statements are emitted at + once in a later step. + + Handlers should **not** modify any attributes which are + mapped by :func:`.relationship`, nor should they attempt + to make any modifications to the :class:`.Session` in + this hook (including :meth:`.Session.add`, + :meth:`.Session.delete`, etc.) - such changes will not + take effect. For overall changes to the "flush plan", + use :meth:`.SessionEvents.before_flush`. + + :param mapper: the :class:`.Mapper` which is the target + of this event. + :param connection: the :class:`.Connection` being used to + emit DELETE statements for this instance. This + provides a handle into the current transaction on the + target database specific to this instance. + :param target: the mapped instance being deleted. If + the event is configured with ``raw=True``, this will + instead be the :class:`.InstanceState` state-management + object associated with the instance. + :return: No return value is supported by this event. + + """ + + def on_after_delete(self, mapper, connection, target): + """Receive an object instance after a DELETE statement + has been emitted corresponding to that instance. + + This event is used to emit additional SQL statements on + the given connection as well as to perform application + specific bookkeeping related to a deletion event. + + The event is often called for a batch of objects of the + same class after their DELETE statements have been emitted at + once in a previous step. + + :param mapper: the :class:`.Mapper` which is the target + of this event. + :param connection: the :class:`.Connection` being used to + emit DELETE statements for this instance. This + provides a handle into the current transaction on the + target database specific to this instance. + :param target: the mapped instance being deleted. If + the event is configured with ``raw=True``, this will + instead be the :class:`.InstanceState` state-management + object associated with the instance. + :return: No return value is supported by this event. + + """ + + @classmethod + def remove(cls, fn, identifier, target): + raise NotImplementedError("Removal of mapper events not yet implemented") + +class SessionEvents(event.Events): + """Define events specific to :class:`.Session` lifecycle. + + e.g.:: + + from sqlalchemy import event + from sqlalchemy.orm import sessionmaker + + class my_before_commit(session): + print "before commit!" + + Session = sessionmaker() + + event.listen(my_before_commit, "on_before_commit", Session) + + The :func:`~.event.listen` function will accept + :class:`.Session` objects as well as the return result + of :func:`.sessionmaker` and :func:`.scoped_session`. + + Additionally, it accepts the :class:`.Session` class which + will apply listeners to all :class:`.Session` instances + globally. + + """ + + @classmethod + def accept_with(cls, target): + from sqlalchemy.orm import ScopedSession, Session + if isinstance(target, ScopedSession): + if not isinstance(target.session_factory, type) or \ + not issubclass(target.session_factory, Session): + raise exc.ArgumentError( + "Session event listen on a ScopedSession " + "requries that its creation callable " + "is a Session subclass.") + return target.session_factory + elif isinstance(target, type): + if issubclass(target, ScopedSession): + return Session + elif issubclass(target, Session): + return target + elif isinstance(target, Session): + return target + else: + return None + + @classmethod + def remove(cls, fn, identifier, target): + raise NotImplementedError("Removal of session events not yet implemented") + + def on_before_commit(self, session): + """Execute before commit is called. + + Note that this may not be per-flush if a longer running + transaction is ongoing.""" + + def on_after_commit(self, session): + """Execute after a commit has occured. + + Note that this may not be per-flush if a longer running + transaction is ongoing.""" + + def on_after_rollback(self, session): + """Execute after a rollback has occured. + + Note that this may not be per-flush if a longer running + transaction is ongoing.""" + + def on_before_flush( self, session, flush_context, instances): + """Execute before flush process has started. + + `instances` is an optional list of objects which were passed to + the ``flush()`` method. """ + + def on_after_flush(self, session, flush_context): + """Execute after flush has completed, but before commit has been + called. + + Note that the session's state is still in pre-flush, i.e. 'new', + 'dirty', and 'deleted' lists still show pre-flush state as well + as the history settings on instance attributes.""" + + def on_after_flush_postexec(self, session, flush_context): + """Execute after flush has completed, and after the post-exec + state occurs. + + This will be when the 'new', 'dirty', and 'deleted' lists are in + their final state. An actual commit() may or may not have + occured, depending on whether or not the flush started its own + transaction or participated in a larger transaction. """ + + def on_after_begin( self, session, transaction, connection): + """Execute after a transaction is begun on a connection + + `transaction` is the SessionTransaction. This method is called + after an engine level transaction is begun on a connection. """ + + def on_after_attach(self, session, instance): + """Execute after an instance is attached to a session. + + This is called after an add, delete or merge. """ + + def on_after_bulk_update( self, session, query, query_context, result): + """Execute after a bulk update operation to the session. + + This is called after a session.query(...).update() + + `query` is the query object that this update operation was + called on. `query_context` was the query context object. + `result` is the result object returned from the bulk operation. + """ + + def on_after_bulk_delete( self, session, query, query_context, result): + """Execute after a bulk delete operation to the session. + + This is called after a session.query(...).delete() + + `query` is the query object that this delete operation was + called on. `query_context` was the query context object. + `result` is the result object returned from the bulk operation. + """ + + +class AttributeEvents(event.Events): + """Define events for object attributes. + + These are typically defined on the class-bound descriptor for the + target class. + + e.g.:: + + from sqlalchemy import event + + def my_append_listener(target, value, initiator): + print "received append event for target: %s" % target + + event.listen(my_append_listener, 'on_append', MyClass.collection) + + Listeners have the option to return a possibly modified version + of the value, when the ``retval=True`` flag is passed + to :func:`~.event.listen`:: + + def validate_phone(target, value, oldvalue, initiator): + "Strip non-numeric characters from a phone number" + + return re.sub(r'(?![0-9])', '', value) + + # setup listener on UserContact.phone attribute, instructing + # it to use the return value + listen(validate_phone, 'on_set', UserContact.phone, retval=True) + + A validation function like the above can also raise an exception + such as :class:`ValueError` to halt the operation. + + Several modifiers are available to the :func:`~.event.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. ++ "on_set" event would like to receive the "old" value being ++ replaced unconditionally, even if this requires firing off ++ database loads. Note that ``active_history`` can also be ++ set directly via :func:`.column_property` and ++ :func:`.relationship`. ++ + :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 not raw or not retval: + orig_fn = fn + def wrap(target, value, *arg): + if not raw: + target = target.obj() + if not retval: + orig_fn(target, value, *arg) + return value + else: + return orig_fn(target, value, *arg) + fn = wrap + + event.Events.listen(fn, identifier, target, propagate) + + if propagate: + from sqlalchemy.orm.instrumentation import manager_of_class + + manager = manager_of_class(target.class_) + + for mgr in manager.subclass_managers(True): + event.Events.listen(fn, identifier, mgr[target.key], True) + + @classmethod + def remove(cls, fn, identifier, target): + raise NotImplementedError("Removal of attribute events not yet implemented") + + def on_append(self, target, value, initiator): + """Receive a collection append event. + + :param target: the object instance receiving the event. + If the listener is registered with ``raw=True``, this will + be the :class:`.InstanceState` object. + :param value: the value being appended. If this listener + 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. + :return: if the event was registered with ``retval=True``, + the given value, or a new effective value, should be returned. + + """ + + def on_remove(self, target, value, initiator): + """Receive a collection remove event. + + :param target: the object instance receiving the event. + 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. + :return: No return value is defined for this event. + """ + + def on_set(self, target, value, oldvalue, initiator): + """Receive a scalar set event. + + :param target: the object instance receiving the event. + If the listener is registered with ``raw=True``, this will + be the :class:`.InstanceState` object. + :param value: the value being set. If this listener + is registered with ``retval=True``, the listener + function must return this value, or a new value which + replaces it. + :param oldvalue: the previous value being replaced. This + may also be the symbol ``NEVER_SET`` or ``NO_VALUE``. + If the listener is registered with ``active_history=True``, + 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. + :return: if the event was registered with ``retval=True``, + the given value, or a new effective value, should be returned. + + """ +