Commits

Mike Bayer committed 66d090b

- Fixed bug in mutable extension as well as
:func:`.attributes.flag_modified` where the change event would not be
propagated if the attribute had been reassigned to itself.
fixes #2997

Conflicts:
lib/sqlalchemy/orm/state.py
test/orm/test_attributes.py

Comments (0)

Files changed (6)

doc/build/changelog/changelog_08.rst

     :version: 0.8.6
 
     .. change::
+        :tags: bug, ext
+        :versions: 0.9.4
+        :tickets: 2997
+
+        Fixed bug in mutable extension as well as
+        :func:`.attributes.flag_modified` where the change event would not be
+        propagated if the attribute had been reassigned to itself.
+
+    .. change::
         :tags: bug, orm
         :versions: 0.9.4
 

lib/sqlalchemy/ext/mutable.py

             outgoing.
 
             """
+            if value is oldvalue:
+                return value
+
             if not isinstance(value, cls):
                 value = cls.coerce(key, value)
             if value is not None:

lib/sqlalchemy/orm/attributes.py

     """
     state, dict_ = instance_state(instance), instance_dict(instance)
     impl = state.manager[key].impl
-    state._modified_event(dict_, impl, NO_VALUE)
+    state._modified_event(dict_, impl, NO_VALUE, force=True)

lib/sqlalchemy/orm/state.py

     def _instance_dict(self):
         return None
 
-    def _modified_event(self, dict_, attr, previous, collection=False):
-        if attr.key not in self.committed_state:
+    def _modified_event(self, dict_, attr, previous, collection=False, force=False):
+        if attr.key not in self.committed_state or force:
             if collection:
                 if previous is NEVER_SET:
                     if attr.key in dict_:

test/ext/test_mutable.py

         sess.commit()
         eq_(f1.data, {'b': 'c'})
 
+    def test_replace_itself_still_ok(self):
+        sess = Session()
+        f1 = Foo(data={'a': 'b'})
+        sess.add(f1)
+        sess.flush()
+
+        f1.data = f1.data
+        f1.data['b'] = 'c'
+        sess.commit()
+        eq_(f1.data, {'a': 'b', 'b': 'c'})
+
     def test_pickle_parent(self):
         sess = Session()
 

test/orm/test_attributes.py

         f.someattr = ['a']
         eq_(self._someattr_history(f), ([['a']], (), ()))
 
+
+    def test_scalar_inplace_mutation_replace_self_flag_modified_set(self):
+        Foo = self._fixture(uselist=False, useobject=False,
+                                active_history=False)
+        f = Foo()
+        f.someattr = {'a': 'b'}
+        self._commit_someattr(f)
+        eq_(self._someattr_history(f), ((), [{'a': 'b'}], ()))
+
+        # set the attribute to itself; this places a copy
+        # in committed_state
+        f.someattr = f.someattr
+
+        attributes.flag_modified(f, 'someattr')
+        eq_(self._someattr_history(f), ([{'a': 'b'}], (), ()))
+
+
     def test_use_object_init(self):
         Foo, Bar = self._two_obj_fixture(uselist=False)
         f = Foo()