|
[Sqlalchemy-commits] sqlalchemy: - add a new "on mapper configured"
event - handy !
From: <commit@sq...> - 2010-12-25 19:08
|
details: http://hg.sqlalchemy.org/sqlalchemy/sqlalchemy/rev/6ddab02e50e9 changeset: 7204:6ddab02e50e9 user: zzzeek date: Sat Dec 25 13:22:12 2010 -0500 description: - add a new "on mapper configured" event - handy ! Subject: sqlalchemy: - on_expire event, since we are starting to build off of events around details: http://hg.sqlalchemy.org/sqlalchemy/sqlalchemy/rev/62647c1c0e1d changeset: 7205:62647c1c0e1d user: zzzeek date: Sat Dec 25 14:08:03 2010 -0500 description: - on_expire event, since we are starting to build off of events around full lifecycle - composite will use events to do everything we want it to, i.e. storing the composite in __dict__, invalidating it on change of any of the columns. - will reinstate mutability of composites via callable attached to the composite - but userland code will still need to establish change event listening on the composite itself, perhaps via a "mutable" mixin like the scalars.py example, perhaps via addition of descriptors to the mutable object. diffstat: examples/mutable_events/__init__.py | 10 +++- examples/mutable_events/scalars.py | 80 +++++++++++++++++++--------- lib/sqlalchemy/orm/descriptor_props.py | 92 +++++++++++++++++++++++++++------ lib/sqlalchemy/orm/events.py | 38 +++++++++++++- lib/sqlalchemy/orm/mapper.py | 1 + lib/sqlalchemy/orm/state.py | 4 + test/orm/test_mapper.py | 6 +- 7 files changed, 181 insertions(+), 50 deletions(-) diffs (truncated from 424 to 300 lines): diff -r 70f7017f6ceb -r 62647c1c0e1d examples/mutable_events/__init__.py --- a/examples/mutable_events/__init__.py Fri Dec 24 11:39:56 2010 -0500 +++ b/examples/mutable_events/__init__.py Sat Dec 25 14:08:03 2010 -0500 @@ -26,6 +26,14 @@ id = Column(Integer, primary_key=True) data = Column(JSONEncodedDict) - MutationDict.listen(Foo.data) + MutationDict.associate_with_attribute(Foo.data) + +The explicit step of associating ``MutationDict`` with ``Foo.data`` can be +automated across a class of columns using ``associate_with_type()``:: + + MutationDict.associate_with_type(JSONEncodedDict) + +All subsequent mappings will have the ``MutationDict`` wrapper applied to +all attributes with ``JSONEncodedDict`` as their type. """ \ No newline at end of file diff -r 70f7017f6ceb -r 62647c1c0e1d examples/mutable_events/scalars.py --- a/examples/mutable_events/scalars.py Fri Dec 24 11:39:56 2010 -0500 +++ b/examples/mutable_events/scalars.py Sat Dec 25 14:08:03 2010 -0500 @@ -1,5 +1,7 @@ from sqlalchemy.orm.attributes import flag_modified from sqlalchemy import event +from sqlalchemy.orm import mapper +from sqlalchemy.util import memoized_property import weakref class TrackMutationsMixin(object): @@ -7,54 +9,78 @@ events to a parent object. """ - _key = None - _parent = None - - def _set_parent(self, parent, key): - self._parent = weakref.ref(parent) - self._key = key + @memoized_property + def _parents(self): + """Dictionary of parent object->attribute name on the parent.""" - def _remove_parent(self): - del self._parent + return weakref.WeakKeyDictionary() - def on_change(self, key=None): + def on_change(self): """Subclasses should call this method whenever change events occur.""" - if key is None: - key = self._key - if self._parent: - p = self._parent() - if p: - flag_modified(p, self._key) + for parent, key in self._parents.items(): + flag_modified(parent, key) @classmethod - def listen(cls, attribute): - """Establish this type as a mutation listener for the given class and - attribute name. + def associate_with_attribute(cls, attribute): + """Establish this type as a mutation listener for the given + mapped descriptor. """ key = attribute.key parent_cls = attribute.class_ def on_load(state): + """Listen for objects loaded or refreshed. + + Wrap the target data member's value with + ``TrackMutationsMixin``. + + """ val = state.dict.get(key, None) if val is not None: val = cls(val) state.dict[key] = val - val._set_parent(state.obj(), key) + val._parents[state.obj()] = key def on_set(target, value, oldvalue, initiator): + """Listen for set/replace events on the target + data member. + + Establish a weak reference to the parent object + on the incoming value, remove it for the one + outgoing. + + """ + if not isinstance(value, cls): value = cls(value) - value._set_parent(target.obj(), key) + value._parents[target.obj()] = key if isinstance(oldvalue, cls): - oldvalue._remove_parent() + oldvalue._parents.pop(state.obj(), None) return value event.listen(parent_cls, 'on_load', on_load, raw=True) event.listen(parent_cls, 'on_refresh', on_load, raw=True) event.listen(attribute, 'on_set', on_set, raw=True, retval=True) - + + @classmethod + def associate_with_type(cls, type_): + """Associate this wrapper with all future mapped columns + of the given type. + + This is a convenience method that calls ``associate_with_attribute`` automatically. + + """ + + def listen_for_type(mapper, class_): + for prop in mapper.iterate_properties: + if hasattr(prop, 'columns') and isinstance(prop.columns[0].type, type_): + cls.listen(getattr(class_, prop.key)) + + event.listen(mapper, 'on_mapper_configured', listen_for_type) + + if __name__ == '__main__': from sqlalchemy import Column, Integer, VARCHAR, create_engine from sqlalchemy.orm import Session @@ -83,7 +109,7 @@ if value is not None: value = simplejson.loads(value, use_decimal=True) return value - + class MutationDict(TrackMutationsMixin, dict): def __init__(self, other): self.update(other) @@ -95,15 +121,15 @@ def __delitem__(self, key): dict.__delitem__(self, key) self.on_change() - + + MutationDict.associate_with_type(JSONEncodedDict) + Base = declarative_base() class Foo(Base): __tablename__ = 'foo' id = Column(Integer, primary_key=True) data = Column(JSONEncodedDict) - - MutationDict.listen(Foo.data) - + e = create_engine('sqlite://', echo=True) Base.metadata.create_all(e) diff -r 70f7017f6ceb -r 62647c1c0e1d lib/sqlalchemy/orm/descriptor_props.py --- a/lib/sqlalchemy/orm/descriptor_props.py Fri Dec 24 11:39:56 2010 -0500 +++ b/lib/sqlalchemy/orm/descriptor_props.py Sat Dec 25 14:08:03 2010 -0500 @@ -15,7 +15,7 @@ from sqlalchemy.orm.interfaces import \ MapperProperty, PropComparator, StrategizedProperty from sqlalchemy.orm import attributes -from sqlalchemy import util, sql, exc as sa_exc +from sqlalchemy import util, sql, exc as sa_exc, event from sqlalchemy.sql import expression properties = util.importlater('sqlalchemy.orm', 'properties') @@ -96,28 +96,94 @@ self.deferred = kwargs.get('deferred', False) self.group = kwargs.get('group', None) util.set_creation_order(self) + self._create_descriptor() + def do_init(self): + """Initialization which occurs after the :class:`.CompositeProperty` + has been associated with its parent mapper. + + """ + self._setup_arguments_on_columns() + self._setup_event_handlers() + + def _create_descriptor(self): + """Create the actual Python descriptor that will serve as + the access point on the mapped class. + + """ + def fget(instance): - # this could be optimized to store the value in __dict__, - # but more complexity and tests would be needed to pick - # up on changes to the mapped columns made independently - # of those on the composite. - return self.composite_class( + dict_ = attributes.instance_dict(instance) + if self.key in dict_: + return dict_[self.key] + else: + dict_[self.key] = composite = self.composite_class( *[getattr(instance, key) for key in self._attribute_keys] ) + return composite def fset(instance, value): if value is None: fdel(instance) else: - for key, value in zip(self._attribute_keys, value.__composite_values__()): + dict_ = attributes.instance_dict(instance) + dict_[self.key] = value + for key, value in zip( + self._attribute_keys, + value.__composite_values__()): setattr(instance, key, value) def fdel(instance): for key in self._attribute_keys: setattr(instance, key, None) + self.descriptor = property(fget, fset, fdel) - + + def _setup_arguments_on_columns(self): + """Propigate configuration arguments made on this composite + to the target columns, for those that apply. + + """ + for col in self.columns: + prop = self.parent._columntoproperty[col] + prop.active_history = self.active_history + if self.deferred: + prop.deferred = self.deferred + prop.strategy_class = strategies.DeferredColumnLoader + prop.group = self.group + + def _setup_event_handlers(self): + """Establish events that will clear out the composite value + whenever changes in state occur on the target columns. + + """ + def load_handler(state): + state.dict.pop(self.key, None) + + def expire_handler(state, keys): + if keys is None or set(self._attribute_keys).intersection(keys): + state.dict.pop(self.key, None) + + def insert_update_handler(mapper, connection, state): + state.dict.pop(self.key, None) + + event.listen(self.parent, 'on_after_insert', + insert_update_handler, raw=True) + event.listen(self.parent, 'on_after_update', + insert_update_handler, raw=True) + event.listen(self.parent, 'on_load', load_handler, raw=True) + event.listen(self.parent, 'on_refresh', load_handler, raw=True) + event.listen(self.parent, "on_expire", expire_handler, raw=True) + + # TODO: add listeners to the column attributes, which + # refresh the composite based on userland settings. + + # TODO: add a callable to the composite of the form + # _on_change(self, attrname) which will send up a corresponding + # refresh to the column attribute on all parents. Basically + # a specialization of the scalars.py example. + + @util.memoized_property def _attribute_keys(self): return [ @@ -155,16 +221,6 @@ (),[self.composite_class(*added)], () ) - def do_init(self): - for col in self.columns: - prop = self.parent._columntoproperty[col] - prop.active_history = self.active_history - if self.deferred: - prop.deferred = self.deferred - prop.strategy_class = strategies.DeferredColumnLoader - prop.group = self.group - # strategies ... - def _comparator_factory(self, mapper): return CompositeProperty.Comparator(self) diff -r 70f7017f6ceb -r 62647c1c0e1d lib/sqlalchemy/orm/events.py --- a/lib/sqlalchemy/orm/events.py Fri Dec 24 11:39:56 2010 -0500 +++ b/lib/sqlalchemy/orm/events.py Sat Dec 25 14:08:03 2010 -0500 @@ -144,6 +144,15 @@ This hook is called after expired attributes have been reloaded. """ + |
| Thread | Author | Date |
|---|---|---|
| [Sqlalchemy-commits] sqlalchemy: - add a new "on mapper configured" event - handy ! | <commit@sq...> |