Commits

Mike Bayer committed 60bb024

- An exception is raised 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. Will commit as a warning in 0.6.
[ticket:2046]

  • Participants
  • Parent commits 62ff273

Comments (0)

Files changed (4)

   - Session weak_instance_dict=False is deprecated.
     [ticket:1473]
 
+  - An exception is raised 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.  Was a warning in 0.6.6.
+    [ticket:2046]
+
 - sql
   - LIMIT/OFFSET clauses now use bind parameters
     [ticket:805]
    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]

File lib/sqlalchemy/orm/exc.py

 class UnmappedError(sa.exc.InvalidRequestError):
     """Base for exceptions that involve expected mappings not present."""
 
+class ObjectDereferencedError(sa.exc.SQLAlchemyError):
+    """An operation cannot complete due to an object being garbage collected."""
+
 class DetachedInstanceError(sa.exc.SQLAlchemyError):
     """An attempt to access unloaded attributes on a 
     mapped instance that is detached."""

File lib/sqlalchemy/orm/state.py

 import weakref
 from sqlalchemy import util
 
-from sqlalchemy.orm import exc as orm_exc, attributes, interfaces
+from sqlalchemy.orm import exc as orm_exc, attributes, interfaces,\
+        util as orm_util
 from sqlalchemy.orm.attributes import PASSIVE_OFF, PASSIVE_NO_RESULT, \
     PASSIVE_NO_FETCH, NEVER_SET, ATTR_WAS_SET, NO_VALUE
 
                 instance_dict._modified.add(self)
 
             self._strong_obj = self.obj()
-
+            if self._strong_obj is None:
+                raise orm_exc.ObjectDereferencedError(
+                        "Can't emit change event for attribute '%s' - parent object "
+                        "of type %s has been garbage collected." 
+                        % (self.manager[attr.key], orm_util.state_class_str(self)))
             self.modified = True
 
     def commit(self, dict_, keys):

File test/orm/test_attributes.py

 import pickle
-from sqlalchemy.orm import attributes, instrumentation
+from sqlalchemy.orm import attributes, instrumentation, exc as orm_exc
 from sqlalchemy.orm.collections import collection
 from sqlalchemy.orm.interfaces import AttributeExtension
 from sqlalchemy import exc as sa_exc
 from test.lib import *
-from test.lib.testing import eq_, ne_, assert_raises
+from test.lib.testing import eq_, ne_, assert_raises, assert_raises_message
 from test.orm import _base
 from test.lib.util import gc_collect, all_partial_orderings
 from sqlalchemy.util import cmp, jython, topological
         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()
+
+        instrumentation.register_class(Foo)
+        instrumentation.register_class(Bar)
+        attributes.register_attribute(Foo, 
+                                    'bars', 
+                                    uselist=True, 
+                                    useobject=True)
+
+        assert_raises_message(
+            orm_exc.ObjectDereferencedError,
+            "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