mirror of
https://github.com/sqlalchemy/sqlalchemy.git
synced 2026-05-28 11:35:19 -04:00
- 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.
This commit is contained in:
@@ -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.
|
||||
def set(self, state, dict_, value, initiator=None,
|
||||
passive=PASSIVE_OFF, pop=False, _adapt=True):
|
||||
iterable = orig_iterable = value
|
||||
|
||||
"""
|
||||
|
||||
self._set_iterable(
|
||||
state, dict_, value,
|
||||
lambda adapter, i: adapter.adapt_like_to_iterable(i))
|
||||
|
||||
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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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_))
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user