.. changelog::
:version: 1.1.8
+ .. change:: 3950
+ :tags: bug, ext
+ :versions: 1.2.0b1
+ :tickets: 3950
+
+ Fixed bug in :mod:`sqlalchemy.ext.mutable` where the
+ :meth:`.Mutable.as_mutable` method would not track a type that had
+ been copied using :meth:`.TypeEngine.copy`. This became more of
+ a regression in 1.1 compared to 1.0 because the :class:`.TypeDecorator`
+ class is now a subclass of :class:`.SchemaEventTarget`, which among
+ other things indicates to the parent :class:`.Column` that the type
+ should be copied when the :class:`.Column` is. These copies are
+ common when using declarative with mixins or abstract classes.
+
.. change::
:tags: bug, ext
:versions: 1.2.0b1
from .. import event, types
from ..orm import mapper, object_mapper, Mapper
from ..util import memoized_property
+from ..sql.base import SchemaEventTarget
import weakref
"""
sqltype = types.to_instance(sqltype)
+ # a SchemaType will be copied when the Column is copied,
+ # and we'll lose our ability to link that type back to the original.
+ # so track our original type w/ columns
+ if isinstance(sqltype, SchemaEventTarget):
+ @event.listens_for(sqltype, "before_parent_attach")
+ def _add_column_memo(sqltyp, parent):
+ parent.info['_ext_mutable_orig_type'] = sqltyp
+ schema_event_check = True
+ else:
+ schema_event_check = False
+
def listen_for_type(mapper, class_):
for prop in mapper.column_attrs:
- if prop.columns[0].type is sqltype:
+ if (
+ schema_event_check and
+ prop.columns[0].info.get('_ext_mutable_orig_type')
+ is sqltype
+ ) or (
+ prop.columns[0].type is sqltype
+ ):
cls.associate_with_attribute(getattr(class_, prop.key))
event.listen(mapper, 'mapper_configured', listen_for_type)
self._test_non_mutable()
+class MutableColumnCopyJSONTest(_MutableDictTestBase, fixtures.MappedTest):
+
+ @classmethod
+ def define_tables(cls, metadata):
+ import json
+ from sqlalchemy.ext.declarative import declarative_base
+
+ class JSONEncodedDict(TypeDecorator):
+ impl = VARCHAR(50)
+
+ def process_bind_param(self, value, dialect):
+ if value is not None:
+ value = json.dumps(value)
+
+ return value
+
+ def process_result_value(self, value, dialect):
+ if value is not None:
+ value = json.loads(value)
+ return value
+
+ MutableDict = cls._type_fixture()
+
+ Base = declarative_base(metadata=metadata)
+
+ class AbstractFoo(Base):
+ __abstract__ = True
+
+ id = Column(Integer, primary_key=True,
+ test_needs_autoincrement=True)
+ data = Column(MutableDict.as_mutable(JSONEncodedDict))
+ non_mutable_data = Column(JSONEncodedDict)
+ unrelated_data = Column(String(50))
+
+ class Foo(AbstractFoo):
+ __tablename__ = "foo"
+
+ assert Foo.data.property.columns[0].type is not AbstractFoo.data.type
+
+ def test_non_mutable(self):
+ self._test_non_mutable()
+
+
class MutableListWithScalarPickleTest(_MutableListTestBase,
fixtures.MappedTest):