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
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):
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
mapped descriptor.
"""
- cls._listen_on_attribute(attribute, True)
+ cls._listen_on_attribute(attribute, True, attribute.class_)
@classmethod
def associate_with(cls, sqltype):
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)
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
-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
class Foo(fixtures.BasicEntity):
pass
+class SubFoo(Foo):
+ pass
+
class _MutableDictTestBase(object):
run_define_tables = 'each'
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):
ClassManager.dispatch._clear()
super(_CompositeTestBase, self).teardown()
-class MutableCompositesTest(_CompositeTestBase, fixtures.MappedTest):
-
@classmethod
def _type_fixture(cls):
other.y == self.y
return Point
+class MutableCompositesTest(_CompositeTestBase, fixtures.MappedTest):
+
@classmethod
def setup_mappers(cls):
foo = cls.tables.foo
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
+