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,
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):
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
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)
"""
- 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.")
"""
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."""
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)
"""
- if not isinstance(values, list):
- values = list(values)
+
+ assert isinstance(values, list)
idset = util.IdentitySet
existing_idset = idset(existing_adapter or ())
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):
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_))
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: