]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- [bug] Fixed bug whereby user error in related-object
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 13 Aug 2012 00:37:40 +0000 (20:37 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 13 Aug 2012 00:37:40 +0000 (20:37 -0400)
assignment could cause recursion overflow if the
assignment triggered a backref of the same name
as a bi-directional attribute on the incorrect
class to the same target.  An informative
error is raised now.

CHANGES
lib/sqlalchemy/orm/attributes.py
test/orm/test_attributes.py

diff --git a/CHANGES b/CHANGES
index cd88f62a51ab930827563cfd797662f6314f7b0e..9830420a41306b188cc905b5616d8ee044a44731 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -471,6 +471,13 @@ are also present in 0.8.
     is combined with uselist=False.  This is an
     exception raise in 0.8.
 
+  - [bug] Fixed bug whereby user error in related-object
+    assignment could cause recursion overflow if the
+    assignment triggered a backref of the same name
+    as a bi-directional attribute on the incorrect
+    class to the same target.  An informative
+    error is raised now.
+
 - sql
   - [bug] Fixed CTE bug whereby positional
     bound parameters present in the CTEs themselves
index 2e576b4d81640aaa96f673109140e0c96b9edc32..d26ee61c34cc5c1bae43af6131847f619a68e4e4 100644 (file)
@@ -983,6 +983,14 @@ def backref_listeners(attribute, key, uselist):
 
     # use easily recognizable names for stack traces
 
+    parent_token = attribute.impl.parent_token
+
+    def _acceptable_key_err(child_state, initiator):
+        raise ValueError(
+            "Object %s not associated with attribute of "
+            "type %s" % (orm_util.state_str(child_state),
+                    manager_of_class(initiator.class_)[initiator.key]))
+
     def emit_backref_from_scalar_set_event(state, child, oldchild, initiator):
         if oldchild is child:
             return child
@@ -1001,35 +1009,47 @@ def backref_listeners(attribute, key, uselist):
         if child is not None:
             child_state, child_dict = instance_state(child),\
                                         instance_dict(child)
-            child_state.manager[key].impl.append(
-                                            child_state,
-                                            child_dict,
-                                            state.obj(),
-                                            initiator,
-                                            passive=PASSIVE_NO_FETCH)
+            child_impl = child_state.manager[key].impl
+            if initiator.parent_token is not parent_token and \
+                initiator.parent_token is not child_impl.parent_token:
+                _acceptable_key_err(state, initiator)
+            child_impl.append(
+                                child_state,
+                                child_dict,
+                                state.obj(),
+                                initiator,
+                                passive=PASSIVE_NO_FETCH)
         return child
 
     def emit_backref_from_collection_append_event(state, child, initiator):
         child_state, child_dict = instance_state(child), \
                                     instance_dict(child)
-        child_state.manager[key].impl.append(
-                                            child_state,
-                                            child_dict,
-                                            state.obj(),
-                                            initiator,
-                                            passive=PASSIVE_NO_FETCH)
+        child_impl = child_state.manager[key].impl
+        if initiator.parent_token is not parent_token and \
+            initiator.parent_token is not child_impl.parent_token:
+            _acceptable_key_err(state, initiator)
+        child_impl.append(
+                                child_state,
+                                child_dict,
+                                state.obj(),
+                                initiator,
+                                passive=PASSIVE_NO_FETCH)
         return child
 
     def emit_backref_from_collection_remove_event(state, child, initiator):
         if child is not None:
             child_state, child_dict = instance_state(child),\
                                         instance_dict(child)
-            child_state.manager[key].impl.pop(
-                                            child_state,
-                                            child_dict,
-                                            state.obj(),
-                                            initiator,
-                                            passive=PASSIVE_NO_FETCH)
+            child_impl = child_state.manager[key].impl
+            # can't think of a path that would produce an initiator
+            # mismatch here, as it would require an existing collection
+            # mismatch.
+            child_impl.pop(
+                                child_state,
+                                child_dict,
+                                state.obj(),
+                                initiator,
+                                passive=PASSIVE_NO_FETCH)
 
     if uselist:
         event.listen(attribute, "append",
index 8a6e37419766dd9bca6853239a513fa38e6ab4cd..546822f5dcf5cef8054df4e3184bf28d7a23d794 100644 (file)
@@ -1158,6 +1158,72 @@ class BackrefTest(fixtures.ORMTest):
         # and this condition changes.
         assert c1 in p1.children
 
+class CyclicBackrefAssertionTest(fixtures.TestBase):
+    """test that infinite recursion due to incorrect backref assignments
+    is blocked.
+
+    """
+    def test_scalar_set_type_assertion(self):
+        A, B, C = self._scalar_fixture()
+        c1 = C()
+        b1 = B()
+        assert_raises_message(
+            ValueError,
+            "Object <B at .*> not associated with attribute of type C.a",
+            setattr, c1, 'a', b1
+        )
+
+    def test_collection_append_type_assertion(self):
+        A, B, C = self._collection_fixture()
+        c1 = C()
+        b1 = B()
+        assert_raises_message(
+            ValueError,
+            "Object <B at .*> not associated with attribute of type C.a",
+            c1.a.append, b1
+        )
+
+    def _scalar_fixture(self):
+        class A(object):
+            pass
+        class B(object):
+            pass
+        class C(object):
+            pass
+        instrumentation.register_class(A)
+        instrumentation.register_class(B)
+        instrumentation.register_class(C)
+        attributes.register_attribute(C, 'a', backref='c', useobject=True)
+        attributes.register_attribute(C, 'b', backref='c', useobject=True)
+
+        attributes.register_attribute(A, 'c', backref='a', useobject=True,
+                        uselist=True)
+        attributes.register_attribute(B, 'c', backref='b', useobject=True,
+                        uselist=True)
+
+        return A, B, C
+
+    def _collection_fixture(self):
+        class A(object):
+            pass
+        class B(object):
+            pass
+        class C(object):
+            pass
+        instrumentation.register_class(A)
+        instrumentation.register_class(B)
+        instrumentation.register_class(C)
+
+        attributes.register_attribute(C, 'a', backref='c', useobject=True,
+                                                uselist=True)
+        attributes.register_attribute(C, 'b', backref='c', useobject=True,
+                                                uselist=True)
+
+        attributes.register_attribute(A, 'c', backref='a', useobject=True)
+        attributes.register_attribute(B, 'c', backref='b', useobject=True)
+
+        return A, B, C
+
 class PendingBackrefTest(fixtures.ORMTest):
     def setup(self):
         global Post, Blog, called, lazy_load