diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/sqlalchemy/orm/attributes.py | 109 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/dynamic.py | 4 |
2 files changed, 88 insertions, 25 deletions
diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index 8268d0816..bb7085402 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -151,6 +151,7 @@ class AttributeImpl(object): value = state.dict[self.key] if value is not NO_VALUE: state.committed_state[self.key] = self.copy(value) + state.pending.pop(self.key, None) def hasparent(self, state, optimistic=False): """Return the boolean value of a `hasparent` flag attached to the given item. @@ -181,7 +182,7 @@ class AttributeImpl(object): current = self.get(state, passive=passive) if current is PASSIVE_NORESULT: return None - return AttributeHistory(self, state, current, passive=passive) + return AttributeHistory(self, state, current) def set_callable(self, state, callable_, clear=False): """Set a callable function for this attribute on the given object. @@ -249,10 +250,10 @@ class AttributeImpl(object): # Return a new, empty value return self.initialize(state) - def append(self, state, value, initiator): + def append(self, state, value, initiator, passive=False): self.set(state, value, initiator) - def remove(self, state, value, initiator): + def remove(self, state, value, initiator, passive=False): self.set(state, None, initiator) def set(self, state, value, initiator): @@ -433,17 +434,27 @@ class CollectionAttributeImpl(AttributeImpl): state.dict[self.key] = user_data return user_data - def append(self, state, value, initiator): + def append(self, state, value, initiator, passive=False): if initiator is self: return - collection = self.get_collection(state) - collection.append_with_event(value, initiator) - def remove(self, state, value, initiator): + collection = self.get_collection(state, passive=passive) + if collection is PASSIVE_NORESULT: + state.get_pending(self).append(value) + self.fire_append_event(state, value, initiator) + else: + collection.append_with_event(value, initiator) + + def remove(self, state, value, initiator, passive=False): if initiator is self: return - collection = self.get_collection(state) - collection.remove_with_event(value, initiator) + + collection = self.get_collection(state, passive=passive) + if collection is PASSIVE_NORESULT: + state.get_pending(self).remove(value) + self.fire_remove_event(state, value, initiator) + else: + collection.remove_with_event(value, initiator) def set(self, state, value, initiator): """Set a value on the given object. @@ -470,7 +481,7 @@ class CollectionAttributeImpl(AttributeImpl): old = self.get(state) old_collection = self.get_collection(state, old) - + new_collection, user_data = self._build_collection(state) idset = util.IdentitySet @@ -494,7 +505,10 @@ class CollectionAttributeImpl(AttributeImpl): old_collection.unlink(old) def set_committed_value(self, state, value): - """Set an attribute value on the given instance and 'commit' it.""" + """Set an attribute value on the given instance and 'commit' it. + + Loads the existing collection from lazy callables in all cases. + """ collection, user_data = self._build_collection(state) self._load_collection(state, value or [], emit_events=False, @@ -509,24 +523,45 @@ class CollectionAttributeImpl(AttributeImpl): return value def _build_collection(self, state): + """build a new, blank collection and return it wrapped in a CollectionAdapter.""" + user_data = self.collection_factory() collection = collections.CollectionAdapter(self, state, user_data) return collection, user_data def _load_collection(self, state, values, emit_events=True, collection=None): + """given an empty CollectionAdapter, load the collection with current values. + + Loads the collection from lazy callables in all cases. + """ + collection = collection or self.get_collection(state) if values is None: return - elif emit_events: + + appender = emit_events and collection.append_with_event or collection.append_without_event + + if self.key in state.pending: + # move 'pending' items into the newly loaded collection + added = state.pending[self.key].added_items + removed = state.pending[self.key].deleted_items for item in values: - collection.append_with_event(item) + if item not in removed: + appender(item) + for item in added: + appender(item) + del state.pending[self.key] else: for item in values: - collection.append_without_event(item) + appender(item) - def get_collection(self, state, user_data=None): + def get_collection(self, state, user_data=None, passive=False): + """retrieve the CollectionAdapter associated with the given state.""" + if user_data is None: - user_data = self.get(state) + user_data = self.get(state, passive=passive) + if user_data is PASSIVE_NORESULT: + return user_data try: return getattr(user_data, '_sa_adapter') except AttributeError: @@ -554,18 +589,18 @@ class GenericBackrefExtension(interfaces.AttributeExtension): # present when updating via a backref. impl = getattr(oldchild.__class__, self.key).impl try: - impl.remove(oldchild._state, instance, initiator) + impl.remove(oldchild._state, instance, initiator, passive=True) except (ValueError, KeyError, IndexError): pass if child is not None: - getattr(child.__class__, self.key).impl.append(child._state, instance, initiator) + getattr(child.__class__, self.key).impl.append(child._state, instance, initiator, passive=True) def append(self, instance, child, initiator): - getattr(child.__class__, self.key).impl.append(child._state, instance, initiator) + getattr(child.__class__, self.key).impl.append(child._state, instance, initiator, passive=True) def remove(self, instance, child, initiator): if child is not None: - getattr(child.__class__, self.key).impl.remove(child._state, instance, initiator) + getattr(child.__class__, self.key).impl.remove(child._state, instance, initiator, passive=True) class ClassState(object): """tracks state information at the class level.""" @@ -577,7 +612,7 @@ class ClassState(object): class InstanceState(object): """tracks state information at the instance level.""" - __slots__ = 'class_', 'obj', 'dict', 'committed_state', 'modified', 'trigger', 'callables', 'parents', 'instance_dict', '_strong_obj', 'expired_attributes' + __slots__ = 'class_', 'obj', 'dict', 'pending', 'committed_state', 'modified', 'trigger', 'callables', 'parents', 'instance_dict', '_strong_obj', 'expired_attributes' def __init__(self, obj): self.class_ = obj.__class__ @@ -588,6 +623,7 @@ class InstanceState(object): self.trigger = None self.callables = {} self.parents = {} + self.pending = {} self.instance_dict = None def __cleanup(self, ref): @@ -627,6 +663,11 @@ class InstanceState(object): finally: instance_dict._mutex.release() + def get_pending(self, attributeimpl): + if attributeimpl.key not in self.pending: + self.pending[attributeimpl.key] = PendingCollection() + return self.pending[attributeimpl.key] + def is_modified(self): if self.modified: return True @@ -654,11 +695,12 @@ class InstanceState(object): return None def __getstate__(self): - return {'committed_state':self.committed_state, 'parents':self.parents, 'modified':self.modified, 'instance':self.obj()} + return {'committed_state':self.committed_state, 'pending':self.pending, 'parents':self.parents, 'modified':self.modified, 'instance':self.obj()} def __setstate__(self, state): self.committed_state = state['committed_state'] self.parents = state['parents'] + self.pending = state['pending'] self.modified = state['modified'] self.obj = weakref.ref(state['instance']) self.class_ = self.obj().__class__ @@ -857,7 +899,7 @@ class AttributeHistory(object): particular instance. """ - def __init__(self, attr, state, current, passive=False): + def __init__(self, attr, state, current): self.attr = attr # get the "original" value. if a lazy load was fired when we got @@ -919,6 +961,27 @@ class AttributeHistory(object): def deleted_items(self): return list(self._deleted_items) +class PendingCollection(object): + """stores items appended and removed from a collection that has not been loaded yet. + + When the collection is loaded, the changes present in PendingCollection are applied + to produce the final result. + """ + + def __init__(self): + self.deleted_items = util.IdentitySet() + self.added_items = util.OrderedIdentitySet() + + def append(self, value): + if value in self.deleted_items: + self.deleted_items.remove(value) + self.added_items.add(value) + + def remove(self, value): + if value in self.added_items: + self.added_items.remove(value) + self.deleted_items.add(value) + def _managed_attributes(class_): """return all InstrumentedAttributes associated with the given class_ and its superclasses.""" diff --git a/lib/sqlalchemy/orm/dynamic.py b/lib/sqlalchemy/orm/dynamic.py index 56cf58d9b..0c49bcfc3 100644 --- a/lib/sqlalchemy/orm/dynamic.py +++ b/lib/sqlalchemy/orm/dynamic.py @@ -44,12 +44,12 @@ class DynamicAttributeImpl(attributes.AttributeImpl): state.dict[self.key] = c = CollectionHistory(self, state) return c - def append(self, state, value, initiator): + def append(self, state, value, initiator, passive=False): if initiator is not self: self.get_history(state)._added_items.append(value) self.fire_append_event(state, value, initiator) - def remove(self, state, value, initiator): + def remove(self, state, value, initiator, passive=False): if initiator is not self: self.get_history(state)._deleted_items.append(value) self.fire_remove_event(state, value, initiator) |
