Commits

Mike Bayer  committed 339f1db

refinements to pending collection, more tests with rollback

  • Participants
  • Parent commits 2483356
  • Branches user_defined_state

Comments (0)

Files changed (3)

File lib/sqlalchemy/orm/attributes.py

         self.impl = impl
         self.comparator = comparator
         self.parententity = parententity
-        mapper, selectable, is_aliased_class = _entity_info(parententity, compile=False)
-        if mapper:
+
+        if parententity:
+            mapper, selectable, is_aliased_class = _entity_info(parententity, compile=False)
             self.property = mapper._get_property(self.impl.key)
         else:
             self.property = None
 
     class ProxyImpl(object):
         accepts_scalar_loader = False
+        
         def __init__(self, key):
             self.key = key
         
 
         An instance attribute that is loaded by a callable function
         will also not have a `hasparent` flag.
+
         """
-
         return state.parents.get(id(self), optimistic)
 
     def sethasparent(self, state, value):
         """Set a boolean flag on the given item corresponding to
         whether or not it is attached to a parent object via the
         attribute represented by this ``InstrumentedAttribute``.
+
         """
-
         state.parents[id(self)] = value
 
     def set_callable(self, state, callable_):
 
         The callable overrides the class level callable set in the
         ``InstrumentedAttribute` constructor.
+
         """
-
         if callable_ is None:
             self.initialize(state)
         else:
     def set(self, state, value, initiator):
         raise NotImplementedError()
 
+    def rollback_to_savepoint(self, state, savepoint):
+        raise NotImplementedError()
+        
     def get_committed_value(self, state):
         """return the unchanged value of this attribute"""
 
 
         state.commit([self.key])
         
-        # ????? removed in merge ?
-        # remove per-instance callable, if any
         state.callables.pop(self.key, None)
         state.dict[self.key] = value
         
         if initiator is self:
             return
         
-        # ???? MERGE ?
-        #if value is not None and not hasattr(value, '_state'):
-        #    raise TypeError("Can not assign %s instance to %s's %r attribute, "
-        #                    "a mapped instance was expected." % (
-        #        type(value).__name__, type(state.obj()).__name__, self.key))
-
-        # TODO: add options to allow the get() to be passive
+        # may want to add options to allow the get() here to be passive
         old = self.get(state)
         state.dict[self.key] = value
         self.fire_replace_event(state, value, old, initiator)
     container object (defaulting to a list) and brokers access to the
     CollectionAdapter, a "view" onto that object that presents consistent
     bag semantics to the orm layer independent of the user data implementation.
+    
     """
     accepts_scalar_loader = False
     uses_objects = True
                     new_collection.append_without_event(item)
                 state.dict[self.key] = user_data
 
-
     def delete(self, state):
         if self.key not in state.dict:
             return
         state.callables.pop(self.key, None)
         state.dict[self.key] = user_data
 
+        state.commit([self.key])
         if self.key in state.pending:
-            # pending items.  commit loaded data, add/remove new data
-            state.committed_state[self.key] = list(value or [])
-            added = state.pending[self.key].added_items
-            removed = state.pending[self.key].deleted_items
+            # pending items exist.  issue a modified event,
+            # add/remove new items.
+            state.modified_event(self, True, user_data)
+
+            pending = state.pending.pop(self.key)
+            added = pending.added_items
+            removed = pending.deleted_items
             for item in added:
                 collection.append_without_event(item)
             for item in removed:
                 collection.remove_without_event(item)
-            del state.pending[self.key]
-        else:
-            state.commit([self.key])
 
         return user_data
 
 
     When the collection is loaded, the changes present in PendingCollection are applied
     to produce the final result.
+    
     """
-
     def __init__(self):
         self.deleted_items = util.IdentitySet()
         self.added_items = util.OrderedIdentitySet()

File test/orm/attr_rollback.py

         assert attributes.has_parent(Foo, b2, 'x', optimistic=False)
         assert not attributes.has_parent(Foo, b3, 'x', optimistic=False)
 
+    def test_pending(self):
+        class CompareByName(object):
+            def __init__(self, name):
+                self.name = name
+            def __eq__(self, other):
+                return other.name == self.name
+            def __hash__(self):
+                return hash(self.name)
+                
+        class Post(CompareByName):
+            pass
+
+        class Blog(CompareByName):
+            pass
+
+        called = [0]
+
+        lazy_load = []
+        def lazy_posts(instance):
+            def load():
+                called[0] += 1
+                return lazy_load
+            return load
+
+        attributes.register_class(Post)
+        attributes.register_class(Blog)
+        attributes.register_attribute(Post, 'blog', uselist=False, extension=attributes.GenericBackrefExtension('posts'), trackparent=True, useobject=True)
+        attributes.register_attribute(Blog, 'posts', uselist=True, extension=attributes.GenericBackrefExtension('blog'), callable_=lazy_posts, trackparent=True, useobject=True, typecallable=make_collection)
+        
+        (p1, p2, p3) = (Post(name='p1'), Post(name='p2'), Post(name='p3'))
+        lazy_load += [p1, p2, p3]
+        
+        b1 = Blog(name='b1')
+        
+        for x in [b1, p1, p2, p3]:
+            attributes.instance_state(x).commit_all(savepoint_id=1)
+        
+        p4 = Post(name='p4')
+        p5 = Post(name='p5')
+        p4.blog = b1
+        p5.blog = b1
+
+        assert b1.posts ==  make_collection([Post(name='p1'), Post(name='p2'), Post(name='p3'), Post(name='p4'), Post(name='p5')])
+        assert attributes.get_history(attributes.instance_state(b1), 'posts') == ([p4, p5], [p1, p2, p3], [])
+
+        for x in [b1, p1, p2, p3]:
+            attributes.instance_state(x).rollback(1)
+        assert attributes.get_history(attributes.instance_state(b1), 'posts') == ([], [p1, p2, p3], [])
+        assert b1.posts == make_collection([p1, p2, p3])
+        
+        # TODO: more tests needed
+
 class ScalarTest(AttrTestBase, TestBase):
     def setUpAll(self):
         global Foo, data1, data2, data3, hist1, hist2, hist3, empty, emptyhist
         empty = make_collection([])
         emptyhist = []
 
+#class DictTest(CollectionTestBase, TestBase):  # TODO
     
 if __name__ == "__main__":
     testenv.main()

File test/orm/attributes.py

         j.port = None
         self.assert_(p.jack is None)
 
-class DeferredBackrefTest(TestBase):
+class PendingBackrefTest(TestBase):
     def setUp(self):
         global Post, Blog, called, lazy_load