From: Jason Kirtland Date: Tue, 1 Apr 2008 16:38:23 +0000 (+0000) Subject: - Light collections refactor, added public collections.bulk_replace. X-Git-Tag: rel_0_4_5~34 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=73a4e9481d893426cedecaba57f06caea1894be1;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - Light collections refactor, added public collections.bulk_replace. - Collection attribs gain some private load-from-iterable flexiblity. --- diff --git a/CHANGES b/CHANGES index 390b478682..1fc3cda0ca 100644 --- 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 diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index 152534d76f..f0b1510f97 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -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. diff --git a/lib/sqlalchemy/orm/collections.py b/lib/sqlalchemy/orm/collections.py index 7b78c240bb..838591d325 100644 --- a/lib/sqlalchemy/orm/collections.py +++ b/lib/sqlalchemy/orm/collections.py @@ -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.