]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- A warning is emitted in the unusual case that an
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 10 Feb 2011 16:42:35 +0000 (11:42 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 10 Feb 2011 16:42:35 +0000 (11:42 -0500)
append or similar event on a collection occurs after
the parent object has been dereferenced, which
prevents the parent from being marked as "dirty"
in the session.  This will be an exception in 0.7.
[ticket:2046]

CHANGES
lib/sqlalchemy/orm/state.py
lib/sqlalchemy/orm/util.py
test/orm/test_attributes.py

diff --git a/CHANGES b/CHANGES
index 380a4881ff319f13e9e36d1af86bef186196e052..67b87b801f36c8c89ea63b2b7517633e12d9439b 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -26,6 +26,13 @@ CHANGES
    or exclude_properties would result in UnmappedColumnError.
    [ticket:1995]
 
+  - A warning is emitted in the unusual case that an 
+    append or similar event on a collection occurs after
+    the parent object has been dereferenced, which 
+    prevents the parent from being marked as "dirty" 
+    in the session.  This will be an exception in 0.7.
+    [ticket:2046]
+
 - sql
   - Column.copy(), as used in table.tometadata(), copies the 
     'doc' attribute.  [ticket:2028]
index 0470a382fdeb833b5ffeac32e017537304d17a73..846ad1328e7c1846349ea182173261bbfedcd1bc 100644 (file)
@@ -8,9 +8,9 @@ from sqlalchemy.util import EMPTY_SET
 import weakref
 from sqlalchemy import util
 from sqlalchemy.orm.attributes import PASSIVE_NO_RESULT, PASSIVE_OFF, \
-                                        NEVER_SET, NO_VALUE, manager_of_class, \
-                                        ATTR_WAS_SET
-from sqlalchemy.orm import attributes, exc as orm_exc, interfaces
+                        NEVER_SET, NO_VALUE, manager_of_class, ATTR_WAS_SET
+from sqlalchemy.orm import attributes, exc as orm_exc, interfaces, \
+                        util as orm_util
 
 import sys
 attributes.state = sys.modules['sqlalchemy.orm.state']
@@ -357,7 +357,16 @@ class InstanceState(object):
                 instance_dict._modified.add(self)
 
             self._strong_obj = self.obj()
-
+            if self._strong_obj is None:
+                util.warn(
+                        "Can't emit change event for attribute '%s.%s' "
+                        "- parent object of type %s has been garbage "
+                        "collected." 
+                        % (
+                            self.class_.__name__, 
+                            attr.key, 
+                            orm_util.state_class_str(self)
+                        ))
             self.modified = True
 
     def commit(self, dict_, keys):
index f26da6c7d1db35c03b0540f8659ee1df9d3b8b49..826e9f35242a2e68555186b11b9eb6bbe6a614b8 100644 (file)
@@ -669,6 +669,14 @@ def state_str(state):
     else:
         return '<%s at 0x%x>' % (state.class_.__name__, id(state.obj()))
 
+def state_class_str(state):
+    """Return a string describing an instance's class via its InstanceState."""
+
+    if state is None:
+        return "None"
+    else:
+        return '<%s>' % (state.class_.__name__, )
+
 def attribute_str(instance, attribute):
     return instance_str(instance) + "." + attribute
 
index 2596268a3f4787917cf6a0b0e19dba0c6c2679a1..217066b0712f63f175c3c34e5040256507aadc30 100644 (file)
@@ -4,7 +4,7 @@ from sqlalchemy.orm.collections import collection
 from sqlalchemy.orm.interfaces import AttributeExtension
 from sqlalchemy import exc as sa_exc
 from sqlalchemy.test import *
-from sqlalchemy.test.testing import eq_, ne_, assert_raises
+from sqlalchemy.test.testing import eq_, ne_, assert_raises, assert_raises_message
 from test.orm import _base
 from sqlalchemy.test.util import gc_collect
 from sqlalchemy.util import cmp, jython
@@ -126,6 +126,28 @@ class AttributesTest(_base.ORMTest):
         assert state.obj() is None
         assert state.dict == {}
 
+    def test_object_dereferenced_error(self):
+        class Foo(object):
+            pass
+        class Bar(object):
+            def __init__(self):
+                gc_collect()
+
+        attributes.register_class(Foo)
+        attributes.register_class(Bar)
+        attributes.register_attribute(Foo, 
+                                    'bars', 
+                                    uselist=True, 
+                                    useobject=True)
+
+        assert_raises_message(
+            sa_exc.SAWarning,
+            "Can't emit change event for attribute "
+            "'Foo.bars' - parent object of type <Foo> "
+            "has been garbage collected.",
+            lambda: Foo().bars.append(Bar())
+        )
+
     def test_deferred(self):
         class Foo(object):pass