]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- on_expire event, since we are starting to build off of events around
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 25 Dec 2010 19:08:03 +0000 (14:08 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 25 Dec 2010 19:08:03 +0000 (14:08 -0500)
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
lib/sqlalchemy/orm/events.py
lib/sqlalchemy/orm/state.py
test/orm/test_mapper.py

index 6fb2d2c57271ce0dc3dda1da040504727b9a4bf9..4c4bc821fb86e3aae5e615dfe5e66bd354375c1a 100644 (file)
@@ -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)
 
index 48b559c635828f162792c1908768989d8899f734..383950add08e252f09cb5b1afe18adb107e884bb 100644 (file)
@@ -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 
index 6ec4239a353c7d96bb61c8b289a54b5d2e0810c6..22be5f58f65f3859f6afdd1e1c2dcc94c830c106 100644 (file)
@@ -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
index d400368f1c692d10385852b4fb38525919aa2707..8f30325554e40d9b60da99a2312cf38a3be61d65 100644 (file)
@@ -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',