From b606e47ddc541952c1d4c1b6d010fc72249af234 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 9 Dec 2015 19:07:51 -0500 Subject: [PATCH] - refactor of adapt_like_to_iterable(), fixes #3457. 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 | 66 +++++++++++++++----------- lib/sqlalchemy/orm/collections.py | 72 ++++++++--------------------- lib/sqlalchemy/orm/dynamic.py | 7 ++- lib/sqlalchemy/orm/relationships.py | 5 +- 4 files changed, 65 insertions(+), 85 deletions(-) diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index 5440d6b5dd..8605df7851 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -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) diff --git a/lib/sqlalchemy/orm/collections.py b/lib/sqlalchemy/orm/collections.py index b9145bae28..58a69227c1 100644 --- a/lib/sqlalchemy/orm/collections.py +++ b/lib/sqlalchemy/orm/collections.py @@ -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): diff --git a/lib/sqlalchemy/orm/dynamic.py b/lib/sqlalchemy/orm/dynamic.py index aedd863f88..ca593765f8 100644 --- a/lib/sqlalchemy/orm/dynamic.py +++ b/lib/sqlalchemy/orm/dynamic.py @@ -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_)) diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py index 1d442eff8c..f822071c47 100644 --- a/lib/sqlalchemy/orm/relationships.py +++ b/lib/sqlalchemy/orm/relationships.py @@ -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: -- 2.47.2