]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Repaired new "mutable" extension to propagate
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 4 Jun 2011 23:43:39 +0000 (19:43 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 4 Jun 2011 23:43:39 +0000 (19:43 -0400)
events to subclasses correctly; don't
create multiple event listeners for
subclasses either.  [ticket:2180]

CHANGES
lib/sqlalchemy/ext/mutable.py
lib/sqlalchemy/orm/descriptor_props.py
test/ext/test_mutable.py

diff --git a/CHANGES b/CHANGES
index 78c83955dce98e4fc2fcef2768a9bcce386ba340..3a2dedc4f5afcfafdaf2cbfe2c00727430602a41 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -13,6 +13,11 @@ CHANGES
     up to foreign key nullability.  
     Related to [ticket:1912]
 
+  - Repaired new "mutable" extension to propagate
+    events to subclasses correctly; don't 
+    create multiple event listeners for 
+    subclasses either.  [ticket:2180]
+
 - sql
   - Fixed bug whereby metadata.reflect(bind)
     would close a Connection passed as a
index 078f9f3a2d89b9ad2ac27dc75ebbaebd6d28a97f..39204f59d57efe48856ac00940e1520b04b3fdfa 100644 (file)
@@ -347,12 +347,16 @@ class MutableBase(object):
         return weakref.WeakKeyDictionary()
 
     @classmethod
-    def _listen_on_attribute(cls, attribute, coerce):
+    def _listen_on_attribute(cls, attribute, coerce, parent_cls):
         """Establish this type as a mutation listener for the given 
         mapped descriptor.
 
         """
         key = attribute.key
+        if parent_cls is not attribute.class_:
+            return
+
+        # rely on "propagate" here
         parent_cls = attribute.class_
 
         def load(state, *args):
@@ -398,11 +402,12 @@ class MutableBase(object):
                 for val in state_dict['ext.mutable.values']:
                     val._parents[state.obj()] = key
 
-        event.listen(parent_cls, 'load', load, raw=True)
-        event.listen(parent_cls, 'refresh', load, raw=True)
-        event.listen(attribute, 'set', set, raw=True, retval=True)
-        event.listen(parent_cls, 'pickle', pickle, raw=True)
-        event.listen(parent_cls, 'unpickle', unpickle, raw=True)
+
+        event.listen(parent_cls, 'load', load, raw=True, propagate=True)
+        event.listen(parent_cls, 'refresh', load, raw=True, propagate=True)
+        event.listen(attribute, 'set', set, raw=True, retval=True, propagate=True)
+        event.listen(parent_cls, 'pickle', pickle, raw=True, propagate=True)
+        event.listen(parent_cls, 'unpickle', unpickle, raw=True, propagate=True)
 
 class Mutable(MutableBase):
     """Mixin that defines transparent propagation of change
@@ -434,7 +439,7 @@ class Mutable(MutableBase):
         mapped descriptor.
 
         """
-        cls._listen_on_attribute(attribute, True)
+        cls._listen_on_attribute(attribute, True, attribute.class_)
 
     @classmethod
     def associate_with(cls, sqltype):
@@ -548,7 +553,7 @@ class MutableComposite(MutableBase):
         def listen_for_type(mapper, class_):
             for prop in mapper.iterate_properties:
                 if hasattr(prop, 'composite_class') and issubclass(prop.composite_class, cls):
-                    cls._listen_on_attribute(getattr(class_, prop.key), False)
+                    cls._listen_on_attribute(getattr(class_, prop.key), False, class_)
 
         event.listen(mapper, 'mapper_configured', listen_for_type)
 
index 5ad148a7eef2c7da2231edaf83f0114d9bbddf13..311993c0d72b8484e1c7297c33994e13aaf291c3 100644 (file)
@@ -215,9 +215,9 @@ class CompositeProperty(DescriptorProperty):
                                     insert_update_handler, raw=True)
         event.listen(self.parent, 'after_update', 
                                     insert_update_handler, raw=True)
-        event.listen(self.parent, 'load', load_handler, raw=True)
-        event.listen(self.parent, 'refresh', load_handler, raw=True)
-        event.listen(self.parent, "expire", expire_handler, raw=True)
+        event.listen(self.parent, 'load', load_handler, raw=True, propagate=True)
+        event.listen(self.parent, 'refresh', load_handler, raw=True, propagate=True)
+        event.listen(self.parent, "expire", expire_handler, raw=True, propagate=True)
 
         # TODO: need a deserialize hook here
 
index 494e4634715af61b88a6487578313735d4ea6818..dec955bab07df074c54e66be8ab01b92ee41d545 100644 (file)
@@ -1,4 +1,4 @@
-from sqlalchemy import Integer
+from sqlalchemy import Integer, ForeignKey
 from sqlalchemy.types import PickleType, TypeDecorator, VARCHAR
 from sqlalchemy.orm import mapper, Session, composite
 from sqlalchemy.orm.mapper import Mapper
@@ -14,6 +14,9 @@ import pickle
 class Foo(fixtures.BasicEntity):
     pass
 
+class SubFoo(Foo):
+    pass
+
 class _MutableDictTestBase(object):
     run_define_tables = 'each'
 
@@ -171,6 +174,51 @@ class MutableWithScalarJSONTest(_MutableDictTestBase, fixtures.MappedTest):
     def test_non_mutable(self):
         self._test_non_mutable()
 
+class MutableAssocWithAttrInheritTest(_MutableDictTestBase, fixtures.MappedTest):
+    @classmethod
+    def define_tables(cls, metadata):
+        MutationDict = cls._type_fixture()
+
+        Table('foo', metadata,
+            Column('id', Integer, primary_key=True, test_needs_autoincrement=True),
+            Column('data', PickleType),
+            Column('non_mutable_data', PickleType)
+        )
+
+        Table('subfoo', metadata,
+            Column('id', Integer, ForeignKey('foo.id'), primary_key=True),
+        )
+
+    def setup_mappers(cls):
+        foo = cls.tables.foo
+        subfoo = cls.tables.subfoo
+
+        mapper(Foo, foo)
+        mapper(SubFoo, subfoo, inherits=Foo)
+        MutationDict.associate_with_attribute(Foo.data)
+
+    def test_in_place_mutation(self):
+        sess = Session()
+
+        f1 = SubFoo(data={'a':'b'})
+        sess.add(f1)
+        sess.commit()
+
+        f1.data['a'] = 'c'
+        sess.commit()
+
+        eq_(f1.data, {'a':'c'})
+
+    def test_replace(self):
+        sess = Session()
+        f1 = SubFoo(data={'a':'b'})
+        sess.add(f1)
+        sess.flush()
+
+        f1.data = {'b':'c'}
+        sess.commit()
+        eq_(f1.data, {'b':'c'})
+
 class MutableAssociationScalarPickleTest(_MutableDictTestBase, fixtures.MappedTest):
     @classmethod
     def define_tables(cls, metadata):
@@ -228,8 +276,6 @@ class _CompositeTestBase(object):
         ClassManager.dispatch._clear()
         super(_CompositeTestBase, self).teardown()
 
-class MutableCompositesTest(_CompositeTestBase, fixtures.MappedTest):
-
     @classmethod
     def _type_fixture(cls):
 
@@ -264,6 +310,8 @@ class MutableCompositesTest(_CompositeTestBase, fixtures.MappedTest):
                     other.y == self.y
         return Point
 
+class MutableCompositesTest(_CompositeTestBase, fixtures.MappedTest):
+
     @classmethod
     def setup_mappers(cls):
         foo = cls.tables.foo
@@ -303,3 +351,59 @@ class MutableCompositesTest(_CompositeTestBase, fixtures.MappedTest):
             sess.add(f2)
             f2.data.y = 12
             assert f2 in sess.dirty
+
+
+class MutableInheritedCompositesTest(_CompositeTestBase, fixtures.MappedTest):
+    @classmethod
+    def define_tables(cls, metadata):
+        Table('foo', metadata,
+            Column('id', Integer, primary_key=True, test_needs_autoincrement=True),
+            Column('x', Integer),
+            Column('y', Integer)
+        )
+        Table('subfoo', metadata,
+            Column('id', Integer, ForeignKey('foo.id'), primary_key=True),
+        )
+
+    @classmethod
+    def setup_mappers(cls):
+        foo = cls.tables.foo
+        subfoo = cls.tables.subfoo
+
+        Point = cls._type_fixture()
+
+        mapper(Foo, foo, properties={
+            'data':composite(Point, foo.c.x, foo.c.y)
+        })
+        mapper(SubFoo, subfoo, inherits=Foo)
+
+    def test_in_place_mutation_subclass(self):
+        sess = Session()
+        d = Point(3, 4)
+        f1 = SubFoo(data=d)
+        sess.add(f1)
+        sess.commit()
+
+        f1.data.y = 5
+        sess.commit()
+
+        eq_(f1.data, Point(3, 5))
+
+    def test_pickle_of_parent_subclass(self):
+        sess = Session()
+        d = Point(3, 4)
+        f1 = SubFoo(data=d)
+        sess.add(f1)
+        sess.commit()
+
+        f1.data
+        assert 'data' in f1.__dict__
+        sess.close()
+
+        for loads, dumps in picklers():
+            sess = Session()
+            f2 = loads(dumps(f1))
+            sess.add(f2)
+            f2.data.y = 12
+            assert f2 in sess.dirty
+