]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- refactor of adapt_like_to_iterable(), fixes #3457.
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 10 Dec 2015 00:07:51 +0000 (19:07 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 10 Dec 2015 00:15:12 +0000 (19:15 -0500)
Includes removal of adapt_like_to_iterable() as well
as _set_iterable(), uses __slots__ for collectionadapter,
does much less duck typing of collections.

lib/sqlalchemy/orm/attributes.py
lib/sqlalchemy/orm/collections.py
lib/sqlalchemy/orm/dynamic.py
lib/sqlalchemy/orm/relationships.py

index 5440d6b5dd2752aa3264a8a75ae8f547be05538a..8605df7851bd31d86fe0f8083de860c3b508c91e 100644 (file)
@@ -853,7 +853,10 @@ class CollectionAttributeImpl(AttributeImpl):
     supports_population = True
     collection = True
 
-    __slots__ = 'copy', 'collection_factory', '_append_token', '_remove_token'
+    __slots__ = (
+        'copy', 'collection_factory', '_append_token', '_remove_token',
+        '_duck_typed_as'
+    )
 
     def __init__(self, class_, key, callable_, dispatch,
                  typecallable=None, trackparent=False, extension=None,
@@ -873,6 +876,8 @@ class CollectionAttributeImpl(AttributeImpl):
         self.collection_factory = typecallable
         self._append_token = None
         self._remove_token = None
+        self._duck_typed_as = util.duck_type_collection(
+            self.collection_factory())
 
         if getattr(self.collection_factory, "_sa_linker", None):
 
@@ -1016,38 +1021,46 @@ class CollectionAttributeImpl(AttributeImpl):
         except (ValueError, KeyError, IndexError):
             pass
 
-    def set(self, state, dict_, value, initiator,
-            passive=PASSIVE_OFF, pop=False):
-        """Set a value on the given object.
-
-        """
-
-        self._set_iterable(
-            state, dict_, value,
-            lambda adapter, i: adapter.adapt_like_to_iterable(i))
+    def set(self, state, dict_, value, initiator=None,
+            passive=PASSIVE_OFF, pop=False, _adapt=True):
+        iterable = orig_iterable = value
 
-    def _set_iterable(self, state, dict_, iterable, adapter=None):
-        """Set a collection value from an iterable of state-bearers.
-
-        ``adapter`` is an optional callable invoked with a CollectionAdapter
-        and the iterable.  Should return an iterable of state-bearing
-        instances suitable for appending via a CollectionAdapter.  Can be used
-        for, e.g., adapting an incoming dictionary into an iterator of values
-        rather than keys.
-
-        """
         # 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._initialize_collection(state)
-        if adapter:
-            new_values = list(adapter(new_collection, iterable))
-        else:
-            new_values = list(iterable)
+        if _adapt:
+            if new_collection._converter is not None:
+                iterable = new_collection._converter(iterable)
+            else:
+                setting_type = util.duck_type_collection(iterable)
+                receiving_type = self._duck_typed_as
+
+                if setting_type is not receiving_type:
+                    given = iterable is None and 'None' or \
+                        iterable.__class__.__name__
+                    wanted = self._duck_typed_as.__name__
+                    raise TypeError(
+                        "Incompatible collection type: %s is not %s-like" % (
+                            given, wanted))
+
+                # If the object is an adapted collection, return the (iterable)
+                # adapter.
+                if hasattr(iterable, '_sa_iterator'):
+                    iterable = iterable._sa_iterator()
+                elif setting_type is dict:
+                    if util.py3k:
+                        iterable = iterable.values()
+                    else:
+                        iterable = getattr(
+                            iterable, 'itervalues', iterable.values)()
+                else:
+                    iterable = iter(iterable)
+        new_values = list(iterable)
 
         old = self.get(state, dict_, passive=PASSIVE_ONLY_PERSISTENT)
         if old is PASSIVE_NO_RESULT:
             old = self.initialize(state, dict_)
-        elif old is iterable:
+        elif old is orig_iterable:
             # ignore re-assignment of the current collection, as happens
             # implicitly with in-place operators (foo.collection |= other)
             return
@@ -1059,7 +1072,8 @@ class CollectionAttributeImpl(AttributeImpl):
 
         dict_[self.key] = user_data
 
-        collections.bulk_replace(new_values, old_collection, new_collection)
+        collections.bulk_replace(
+            new_values, old_collection, new_collection)
 
         del old._sa_adapter
         self.dispatch.dispose_collection(state, old, old_collection)
index b9145bae288b184cdd1fc355adadf04a13ef2372..58a69227c1d831dea728476837fd67fd6e1e1c8e 100644 (file)
@@ -574,13 +574,18 @@ class CollectionAdapter(object):
 
 
     """
-    invalidated = False
+
+    __slots__ = (
+        'attr', '_key', '_data', 'owner_state', '_converter', 'invalidated')
 
     def __init__(self, attr, owner_state, data):
+        self.attr = attr
         self._key = attr.key
         self._data = weakref.ref(data)
         self.owner_state = owner_state
         data._sa_adapter = self
+        self._converter = data._sa_converter
+        self.invalidated = False
 
     def _warn_invalidated(self):
         util.warn("This collection has been invalidated.")
@@ -600,53 +605,8 @@ class CollectionAdapter(object):
         """
         return self.owner_state.dict[self._key] is self._data()
 
-    @util.memoized_property
-    def attr(self):
-        return self.owner_state.manager[self._key].impl
-
-    def adapt_like_to_iterable(self, obj):
-        """Converts collection-compatible objects to an iterable of values.
-
-        Can be passed any type of object, and if the underlying collection
-        determines that it can be adapted into a stream of values it can
-        use, returns an iterable of values suitable for append()ing.
-
-        This method may raise TypeError or any other suitable exception
-        if adaptation fails.
-
-        If a converter implementation is not supplied on the collection,
-        a default duck-typing-based implementation is used.
-
-        """
-        converter = self._data()._sa_converter
-        if converter is not None:
-            return converter(obj)
-
-        setting_type = util.duck_type_collection(obj)
-        receiving_type = util.duck_type_collection(self._data())
-
-        if obj is None or setting_type != receiving_type:
-            given = obj is None and 'None' or obj.__class__.__name__
-            if receiving_type is None:
-                wanted = self._data().__class__.__name__
-            else:
-                wanted = receiving_type.__name__
-
-            raise TypeError(
-                "Incompatible collection type: %s is not %s-like" % (
-                    given, wanted))
-
-        # If the object is an adapted collection, return the (iterable)
-        # adapter.
-        if getattr(obj, '_sa_adapter', None) is not None:
-            return obj._sa_adapter
-        elif setting_type == dict:
-            if util.py3k:
-                return obj.values()
-            else:
-                return getattr(obj, 'itervalues', obj.values)()
-        else:
-            return iter(obj)
+    def bulk_appender(self):
+        return self._data()._sa_appender
 
     def append_with_event(self, item, initiator=None):
         """Add an entity to the collection, firing mutation events."""
@@ -663,6 +623,9 @@ class CollectionAdapter(object):
         for item in items:
             appender(item, _sa_initiator=False)
 
+    def bulk_remover(self):
+        return self._data()._sa_remover
+
     def remove_with_event(self, item, initiator=None):
         """Remove an entity from the collection, firing mutation events."""
         self._data()._sa_remover(item, _sa_initiator=initiator)
@@ -777,8 +740,8 @@ def bulk_replace(values, existing_adapter, new_adapter):
 
 
     """
-    if not isinstance(values, list):
-        values = list(values)
+
+    assert isinstance(values, list)
 
     idset = util.IdentitySet
     existing_idset = idset(existing_adapter or ())
@@ -786,15 +749,18 @@ def bulk_replace(values, existing_adapter, new_adapter):
     additions = idset(values or ()).difference(constants)
     removals = existing_idset.difference(constants)
 
+    appender = new_adapter.bulk_appender()
+
     for member in values or ():
         if member in additions:
-            new_adapter.append_with_event(member)
+            appender(member)
         elif member in constants:
-            new_adapter.append_without_event(member)
+            appender(member, _sa_initiator=False)
 
     if existing_adapter:
+        remover = existing_adapter.bulk_remover()
         for member in removals:
-            existing_adapter.remove_with_event(member)
+            remover(member)
 
 
 def prepare_instrumentation(factory):
index aedd863f88049cf1acbe3b4b6e15076d23570970..ca593765f8b07906059be83ee025ac5147f5da0a 100644 (file)
@@ -128,17 +128,16 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
         dict_[self.key] = True
         return state.committed_state[self.key]
 
-    def set(self, state, dict_, value, initiator,
+    def set(self, state, dict_, value, initiator=None,
             passive=attributes.PASSIVE_OFF,
-            check_old=None, pop=False):
+            check_old=None, pop=False, _adapt=True):
         if initiator and initiator.parent_token is self.parent_token:
             return
 
         if pop and value is None:
             return
-        self._set_iterable(state, dict_, value)
 
-    def _set_iterable(self, state, dict_, iterable, adapter=None):
+        iterable = value
         new_values = list(iterable)
         if state.has_identity:
             old_collection = util.IdentitySet(self.get(state, dict_))
index 1d442eff8c70d30e13bd7ac01f31ffca187731da..f822071c47965e4dd5bf32d6a67acd860f21ed66 100644 (file)
@@ -1476,8 +1476,9 @@ class RelationshipProperty(StrategizedProperty):
                 for c in dest_list:
                     coll.append_without_event(c)
             else:
-                dest_state.get_impl(self.key)._set_iterable(
-                    dest_state, dest_dict, dest_list)
+                dest_state.get_impl(self.key).set(
+                    dest_state, dest_dict, dest_list,
+                    _adapt=False)
         else:
             current = source_dict[self.key]
             if current is not None: