summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES5
-rw-r--r--lib/sqlalchemy/ext/mutable.py21
-rw-r--r--lib/sqlalchemy/orm/descriptor_props.py6
-rw-r--r--test/ext/test_mutable.py110
4 files changed, 128 insertions, 14 deletions
diff --git a/CHANGES b/CHANGES
index 78c83955d..3a2dedc4f 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 078f9f3a2..39204f59d 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 5ad148a7e..311993c0d 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 494e46347..dec955bab 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
+