From 7dbb6bffc20bccb6e7087f9cfddb8463d9178204 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 25 Dec 2010 14:08:03 -0500 Subject: [PATCH] - 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. --- lib/sqlalchemy/orm/descriptor_props.py | 92 +++++++++++++++++++++----- lib/sqlalchemy/orm/events.py | 9 +++ lib/sqlalchemy/orm/state.py | 4 ++ test/orm/test_mapper.py | 6 +- 4 files changed, 91 insertions(+), 20 deletions(-) diff --git a/lib/sqlalchemy/orm/descriptor_props.py b/lib/sqlalchemy/orm/descriptor_props.py index 6fb2d2c572..4c4bc821fb 100644 --- a/lib/sqlalchemy/orm/descriptor_props.py +++ b/lib/sqlalchemy/orm/descriptor_props.py @@ -15,7 +15,7 @@ build on the "hybrid" extension to produce class descriptors. 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 @@ class CompositeProperty(DescriptorProperty): 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 @@ class CompositeProperty(DescriptorProperty): (),[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 --git a/lib/sqlalchemy/orm/events.py b/lib/sqlalchemy/orm/events.py index 48b559c635..383950add0 100644 --- a/lib/sqlalchemy/orm/events.py +++ b/lib/sqlalchemy/orm/events.py @@ -144,6 +144,15 @@ class InstanceEvents(event.Events): This hook is called after expired attributes have been reloaded. """ + + def on_expire(self, target, keys): + """Receive an object instance after its attributes or some subset + have been expired. + + 'keys' is a list of attribute names. If None, the entire + state was expired. + + """ def on_resurrect(self, target): """Receive an object instance as it is 'resurrected' from diff --git a/lib/sqlalchemy/orm/state.py b/lib/sqlalchemy/orm/state.py index 6ec4239a35..22be5f58f6 100644 --- a/lib/sqlalchemy/orm/state.py +++ b/lib/sqlalchemy/orm/state.py @@ -230,6 +230,8 @@ class InstanceState(object): self.callables[key] = self dict_.pop(key, None) + self.manager.dispatch.on_expire(self, None) + def expire_attributes(self, dict_, attribute_names): pending = self.__dict__.get('pending', None) mutable_dict = self.mutable_dict @@ -246,6 +248,8 @@ class InstanceState(object): if pending: pending.pop(key, None) + self.manager.dispatch.on_expire(self, attribute_names) + def __call__(self, passive): """__call__ allows the InstanceState to act as a deferred callable for loading expired attributes, which is also diff --git a/test/orm/test_mapper.py b/test/orm/test_mapper.py index d400368f1c..8f30325554 100644 --- a/test/orm/test_mapper.py +++ b/test/orm/test_mapper.py @@ -2323,6 +2323,7 @@ class MapperEventsTest(_fixtures.FixtureTest): 'on_populate_instance', 'on_load', 'on_refresh', + 'on_expire', 'on_before_insert', 'on_after_insert', 'on_before_update', @@ -2343,7 +2344,8 @@ class MapperEventsTest(_fixtures.FixtureTest): u = User(name='u1') sess.add(u) sess.flush() - u = sess.query(User).populate_existing().get(u.id) + sess.expire(u) + u = sess.query(User).get(u.id) sess.expunge_all() u = sess.query(User).get(u.id) u.name = 'u1 changed' @@ -2352,7 +2354,7 @@ class MapperEventsTest(_fixtures.FixtureTest): sess.flush() eq_(canary, ['on_init', 'on_before_insert', - 'on_after_insert', 'on_translate_row', 'on_populate_instance', + 'on_after_insert', 'on_expire', 'on_translate_row', 'on_populate_instance', 'on_refresh', 'on_append_result', 'on_translate_row', 'on_create_instance', 'on_populate_instance', 'on_load', 'on_append_result', -- 2.47.2