From: Mike Bayer Date: Thu, 30 Mar 2017 21:52:10 +0000 (-0400) Subject: Track SchemaEventTarget types in as_mutable() X-Git-Tag: rel_1_1_8~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=502a46b64e0434332132af244f76cf834140508e;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Track SchemaEventTarget types in as_mutable() 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-Id: Ib04df862c58263185dbae686c548fea3e12c46f1 Fixes: #3950 (cherry picked from commit 07b63894cb8ff9529b406f196b5d7cb9af209e9e) --- diff --git a/doc/build/changelog/changelog_11.rst b/doc/build/changelog/changelog_11.rst index ce704d7b7b..c4735c5de7 100644 --- a/doc/build/changelog/changelog_11.rst +++ b/doc/build/changelog/changelog_11.rst @@ -21,6 +21,20 @@ .. 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 diff --git a/lib/sqlalchemy/ext/mutable.py b/lib/sqlalchemy/ext/mutable.py index 3361c44750..0e35914613 100644 --- a/lib/sqlalchemy/ext/mutable.py +++ b/lib/sqlalchemy/ext/mutable.py @@ -352,6 +352,7 @@ from ..orm.attributes import flag_modified from .. import event, types from ..orm import mapper, object_mapper, Mapper from ..util import memoized_property +from ..sql.base import SchemaEventTarget import weakref @@ -584,9 +585,26 @@ class Mutable(MutableBase): """ 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) diff --git a/test/ext/test_mutable.py b/test/ext/test_mutable.py index 81d2136e36..5b6d3e7cf7 100644 --- a/test/ext/test_mutable.py +++ b/test/ext/test_mutable.py @@ -725,6 +725,49 @@ class MutableWithScalarJSONTest(_MutableDictTestBase, fixtures.MappedTest): 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):