From: Mike Bayer Date: Sat, 4 Jun 2011 23:43:39 +0000 (-0400) Subject: - Repaired new "mutable" extension to propagate X-Git-Tag: rel_0_7_1~8 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=5cac2468b6ef9e7ab4c0f3400477b697cd2d4ec6;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - Repaired new "mutable" extension to propagate events to subclasses correctly; don't create multiple event listeners for subclasses either. [ticket:2180] --- diff --git a/CHANGES b/CHANGES index 78c83955dc..3a2dedc4f5 100644 --- 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 diff --git a/lib/sqlalchemy/ext/mutable.py b/lib/sqlalchemy/ext/mutable.py index 078f9f3a2d..39204f59d5 100644 --- a/lib/sqlalchemy/ext/mutable.py +++ b/lib/sqlalchemy/ext/mutable.py @@ -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) diff --git a/lib/sqlalchemy/orm/descriptor_props.py b/lib/sqlalchemy/orm/descriptor_props.py index 5ad148a7ee..311993c0d7 100644 --- a/lib/sqlalchemy/orm/descriptor_props.py +++ b/lib/sqlalchemy/orm/descriptor_props.py @@ -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 diff --git a/test/ext/test_mutable.py b/test/ext/test_mutable.py index 494e463471..dec955bab0 100644 --- a/test/ext/test_mutable.py +++ b/test/ext/test_mutable.py @@ -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 +