]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Light collections refactor, added public collections.bulk_replace.
authorJason Kirtland <jek@discorporate.us>
Tue, 1 Apr 2008 16:38:23 +0000 (16:38 +0000)
committerJason Kirtland <jek@discorporate.us>
Tue, 1 Apr 2008 16:38:23 +0000 (16:38 +0000)
- Collection attribs gain some private load-from-iterable flexiblity.

CHANGES
lib/sqlalchemy/orm/attributes.py
lib/sqlalchemy/orm/collections.py

diff --git a/CHANGES b/CHANGES
index 390b478682a99c98ff46c3ea8b2986678b0d3000..1fc3cda0ca1d165aa32ef582f45d36d90b7245d0 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -129,9 +129,13 @@ CHANGES
           relations that are present on superclasses when using
           inheritance.
 
-    - fixed order_by calculation in Query to properly alias
+    - Fixed order_by calculation in Query to properly alias
       mapper-config'ed order_by when using select_from()
 
+    - Refactored the diffing logic that kicks in when replacing
+      one collection with another into collections.bulk_replace,
+      useful to anyone building multi-level collections.
+
 - sql
     - Schema-qualified tables now will place the schemaname
       ahead of the tablename in all column expressions as well
index 152534d76f59692da544e7be54cc6e5c823f604f..f0b1510f9768386283334b9a4e2bcc714906fb20 100644 (file)
@@ -581,18 +581,34 @@ class CollectionAttributeImpl(AttributeImpl):
         if initiator is self:
             return
 
+        self._set_iterable(
+            state, value,
+            lambda adapter, i: adapter.adapt_like_to_iterable(i))
+
+    def _set_iterable(self, state, iterable, adapter=None):
+        """Set a collection value from an interable.
+
+        ``adapter`` is an optional callable invoked with a CollectionAdapter
+        and the iterable.  Should return an iterable of instances suitable for
+        appending via a CollectionAdapter.  Can be used for, e.g., adapting an
+        incoming dictionary iterable to a list.
+
+        """
         # we need a CollectionAdapter to adapt the incoming value to an
         # assignable iterable.  pulling a new collection first so that
         # an adaptation exception does not trigger a lazy load of the
         # old collection.
         new_collection, user_data = self._build_collection(state)
-        new_values = list(new_collection.adapt_like_to_iterable(value))
+        if adapter:
+            new_values = list(adapter(new_collection, iterable))
+        else:
+            new_values = list(iterable)
 
         old = self.get(state)
 
         # ignore re-assignment of the current collection, as happens
         # implicitly with in-place operators (foo.collection |= other)
-        if old is value:
+        if old is iterable:
             return
 
         if self.key not in state.committed_state:
@@ -600,25 +616,12 @@ class CollectionAttributeImpl(AttributeImpl):
 
         old_collection = self.get_collection(state, old)
 
-        idset = util.IdentitySet
-        constants = idset(old_collection or []).intersection(new_values or [])
-        additions = idset(new_values or []).difference(constants)
-        removals  = idset(old_collection or []).difference(constants)
-
-        for member in new_values or ():
-            if member in additions:
-                new_collection.append_with_event(member)
-            elif member in constants:
-                new_collection.append_without_event(member)
-
         state.dict[self.key] = user_data
         state.modified = True
 
-        # mark all the orphaned elements as detached from the parent
-        if old_collection:
-            for member in removals:
-                old_collection.remove_with_event(member)
-            old_collection.unlink(old)
+        collections.bulk_replace(new_values, old_collection, new_collection)
+        old_collection.unlink(old)
+
 
     def set_committed_value(self, state, value):
         """Set an attribute value on the given instance and 'commit' it.
index 7b78c240bb9a671c90b764bba33517d3f9e71d9c..838591d3257ab15a16a08b6b00258ccf170bdccd 100644 (file)
@@ -612,6 +612,43 @@ class CollectionAdapter(object):
         self._data = weakref.ref(d['data'])
 
 
+def bulk_replace(values, existing_adapter, new_adapter):
+    """Load a new collection, firing events based on prior like membership.
+
+    Appends instances in ``values`` onto the ``new_adapter``. Events will be
+    fired for any instance not present in the ``existing_adapter``.  Any
+    instances in ``existing_adapter`` not present in ``values`` will have
+    remove events fired upon them.
+
+    values
+      An iterable of collection member instances
+
+    existing_adapter
+      A CollectionAdapter of instances to be replaced
+
+    new_adapter
+      An empty CollectionAdapter to load with ``values``
+
+
+    """
+    if not isinstance(values, list):
+        values = list(values)
+
+    idset = sautil.IdentitySet
+    constants = idset(existing_adapter or ()).intersection(values or ())
+    additions = idset(values or ()).difference(constants)
+    removals  = idset(existing_adapter or ()).difference(constants)
+
+    for member in values or ():
+        if member in additions:
+            new_adapter.append_with_event(member)
+        elif member in constants:
+            new_adapter.append_without_event(member)
+
+    if existing_adapter:
+        for member in removals:
+            existing_adapter.remove_with_event(member)
+
 __instrumentation_mutex = sautil.threading.Lock()
 def _prepare_instrumentation(factory):
     """Prepare a callable for future use as a collection class factory.