diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/sqlalchemy/orm/attributes.py | 108 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/dynamic.py | 10 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/exc.py | 8 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/state.py | 36 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 4 |
5 files changed, 122 insertions, 44 deletions
diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index f8078cfaa..bfec81c9c 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -17,7 +17,7 @@ import operator from operator import itemgetter from sqlalchemy import util, event, exc as sa_exc -from sqlalchemy.orm import interfaces, collections, events +from sqlalchemy.orm import interfaces, collections, events, exc as orm_exc mapperutil = util.importlater("sqlalchemy.orm", "util") @@ -126,7 +126,7 @@ class QueryableAttribute(interfaces.PropComparator): return op(other, self.comparator, **kwargs) def hasparent(self, state, optimistic=False): - return self.impl.hasparent(state, optimistic=optimistic) + return self.impl.hasparent(state, optimistic=optimistic) is not False def __getattr__(self, key): try: @@ -346,15 +346,44 @@ class AttributeImpl(object): will also not have a `hasparent` flag. """ - return state.parents.get(id(self.parent_token), optimistic) + assert self.trackparent, "This AttributeImpl is not configured to track parents." - def sethasparent(self, state, value): + return state.parents.get(id(self.parent_token), optimistic) \ + is not False + + def sethasparent(self, state, parent_state, value): """Set a boolean flag on the given item corresponding to whether or not it is attached to a parent object via the attribute represented by this ``InstrumentedAttribute``. """ - state.parents[id(self.parent_token)] = value + assert self.trackparent, "This AttributeImpl is not configured to track parents." + + id_ = id(self.parent_token) + if value: + state.parents[id_] = parent_state + else: + if id_ in state.parents: + last_parent = state.parents[id_] + + if last_parent is not False and \ + last_parent.key != parent_state.key: + + if last_parent.obj() is None: + raise orm_exc.StaleDataError( + "Removing state %s from parent " + "state %s along attribute '%s', " + "but the parent record " + "has gone stale, can't be sure this " + "is the most recent parent." % + (mapperutil.state_str(state), + mapperutil.state_str(parent_state), + self.key)) + + return + + state.parents[id_] = False + def set_callable(self, state, callable_): """Set a callable function for this attribute on the given object. @@ -449,9 +478,15 @@ class AttributeImpl(object): self.set(state, dict_, value, initiator, passive=passive) def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF): - self.set(state, dict_, None, initiator, passive=passive) + self.set(state, dict_, None, initiator, + passive=passive, check_old=value) + + def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF): + self.set(state, dict_, None, initiator, + passive=passive, check_old=value, pop=True) - def set(self, state, dict_, value, initiator, passive=PASSIVE_OFF): + def set(self, state, dict_, value, initiator, + passive=PASSIVE_OFF, check_old=None, pop=False): raise NotImplementedError() def get_committed_value(self, state, dict_, passive=PASSIVE_OFF): @@ -497,7 +532,8 @@ class ScalarAttributeImpl(AttributeImpl): return History.from_scalar_attribute( self, state, dict_.get(self.key, NO_VALUE)) - def set(self, state, dict_, value, initiator, passive=PASSIVE_OFF): + def set(self, state, dict_, value, initiator, + passive=PASSIVE_OFF, check_old=None, pop=False): if initiator and initiator.parent_token is self.parent_token: return @@ -575,8 +611,10 @@ class MutableScalarAttributeImpl(ScalarAttributeImpl): ScalarAttributeImpl.delete(self, state, dict_) state.mutable_dict.pop(self.key) - def set(self, state, dict_, value, initiator, passive=PASSIVE_OFF): - ScalarAttributeImpl.set(self, state, dict_, value, initiator, passive) + def set(self, state, dict_, value, initiator, + passive=PASSIVE_OFF, check_old=None, pop=False): + ScalarAttributeImpl.set(self, state, dict_, value, + initiator, passive, check_old=check_old, pop=pop) state.mutable_dict[self.key] = value @@ -627,7 +665,8 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl): else: return [] - def set(self, state, dict_, value, initiator, passive=PASSIVE_OFF): + def set(self, state, dict_, value, initiator, + passive=PASSIVE_OFF, check_old=None, pop=False): """Set a value on the given InstanceState. `initiator` is the ``InstrumentedAttribute`` that initiated the @@ -643,12 +682,24 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl): else: old = self.get(state, dict_, passive=PASSIVE_NO_FETCH) + if check_old is not None and \ + old is not PASSIVE_NO_RESULT and \ + check_old is not old: + if pop: + return + else: + raise ValueError( + "Object %s not associated with %s on attribute '%s'" % ( + mapperutil.instance_str(check_old), + mapperutil.state_str(state), + self.key + )) value = self.fire_replace_event(state, dict_, value, old, initiator) dict_[self.key] = value def fire_remove_event(self, state, dict_, value, initiator): if self.trackparent and value is not None: - self.sethasparent(instance_state(value), False) + self.sethasparent(instance_state(value), state, False) for fn in self.dispatch.remove: fn(state, value, initiator or self) @@ -660,7 +711,7 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl): if (previous is not value and previous is not None and previous is not PASSIVE_NO_RESULT): - self.sethasparent(instance_state(previous), False) + self.sethasparent(instance_state(previous), state, False) for fn in self.dispatch.set: value = fn(state, value, previous, initiator or self) @@ -669,7 +720,7 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl): if self.trackparent: if value is not None: - self.sethasparent(instance_state(value), True) + self.sethasparent(instance_state(value), state, True) return value @@ -751,7 +802,7 @@ class CollectionAttributeImpl(AttributeImpl): state.modified_event(dict_, self, NEVER_SET, True) if self.trackparent and value is not None: - self.sethasparent(instance_state(value), True) + self.sethasparent(instance_state(value), state, True) return value @@ -760,7 +811,7 @@ class CollectionAttributeImpl(AttributeImpl): def fire_remove_event(self, state, dict_, value, initiator): if self.trackparent and value is not None: - self.sethasparent(instance_state(value), False) + self.sethasparent(instance_state(value), state, False) for fn in self.dispatch.remove: fn(state, value, initiator or self) @@ -815,7 +866,17 @@ class CollectionAttributeImpl(AttributeImpl): else: collection.remove_with_event(value, initiator) - def set(self, state, dict_, value, initiator, passive=PASSIVE_OFF): + def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF): + try: + # TODO: better solution here would be to add + # a "popper" role to collections.py to complement + # "remover". + self.remove(state, dict_, value, initiator, passive=passive) + except (ValueError, KeyError, IndexError): + pass + + def set(self, state, dict_, value, initiator, + passive=PASSIVE_OFF, pop=False): """Set a value on the given object. `initiator` is the ``InstrumentedAttribute`` that initiated the @@ -922,13 +983,10 @@ def backref_listeners(attribute, key, uselist): old_state, old_dict = instance_state(oldchild),\ instance_dict(oldchild) impl = old_state.manager[key].impl - try: - impl.remove(old_state, - old_dict, - state.obj(), - initiator, passive=PASSIVE_NO_FETCH) - except (ValueError, KeyError, IndexError): - pass + impl.pop(old_state, + old_dict, + state.obj(), + initiator, passive=PASSIVE_NO_FETCH) if child is not None: child_state, child_dict = instance_state(child),\ @@ -956,7 +1014,7 @@ def backref_listeners(attribute, key, uselist): if child is not None: child_state, child_dict = instance_state(child),\ instance_dict(child) - child_state.manager[key].impl.remove( + child_state.manager[key].impl.pop( child_state, child_dict, state.obj(), diff --git a/lib/sqlalchemy/orm/dynamic.py b/lib/sqlalchemy/orm/dynamic.py index d4a031d9a..27ca2ef4c 100644 --- a/lib/sqlalchemy/orm/dynamic.py +++ b/lib/sqlalchemy/orm/dynamic.py @@ -80,14 +80,14 @@ class DynamicAttributeImpl(attributes.AttributeImpl): value = fn(state, value, initiator or self) if self.trackparent and value is not None: - self.sethasparent(attributes.instance_state(value), True) + self.sethasparent(attributes.instance_state(value), state, True) def fire_remove_event(self, state, dict_, value, initiator): collection_history = self._modified_event(state, dict_) collection_history.deleted_items.append(value) if self.trackparent and value is not None: - self.sethasparent(attributes.instance_state(value), False) + self.sethasparent(attributes.instance_state(value), state, False) for fn in self.dispatch.remove: fn(state, value, initiator or self) @@ -107,12 +107,16 @@ class DynamicAttributeImpl(attributes.AttributeImpl): return state.committed_state[self.key] def set(self, state, dict_, value, initiator, - passive=attributes.PASSIVE_OFF): + passive=attributes.PASSIVE_OFF, + check_old=None, pop=False): 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): collection_history = self._modified_event(state, dict_) new_values = list(iterable) diff --git a/lib/sqlalchemy/orm/exc.py b/lib/sqlalchemy/orm/exc.py index 98b97059e..2accde9e9 100644 --- a/lib/sqlalchemy/orm/exc.py +++ b/lib/sqlalchemy/orm/exc.py @@ -15,7 +15,7 @@ NO_STATE = (AttributeError, KeyError) class StaleDataError(sa.exc.SQLAlchemyError): """An operation encountered database state that is unaccounted for. - Two conditions cause this to happen: + Conditions which cause this to happen include: * A flush may have attempted to update or delete rows and an unexpected number of rows were matched during @@ -27,6 +27,12 @@ class StaleDataError(sa.exc.SQLAlchemyError): * A mapped object with version_id_col was refreshed, and the version number coming back from the database does not match that of the object itself. + + * A object is detached from its parent object, however + the object was previously attached to a different parent + identity which was garbage collected, and a decision + cannot be made if the new parent was really the most + recent "parent" (new in 0.7.4). """ diff --git a/lib/sqlalchemy/orm/state.py b/lib/sqlalchemy/orm/state.py index 76fddc621..dbe388341 100644 --- a/lib/sqlalchemy/orm/state.py +++ b/lib/sqlalchemy/orm/state.py @@ -132,11 +132,11 @@ class InstanceState(object): def __getstate__(self): d = {'instance':self.obj()} - d.update( (k, self.__dict__[k]) for k in ( - 'committed_state', 'pending', 'parents', 'modified', 'expired', - 'callables', 'key', 'load_options', 'mutable_dict' + 'committed_state', 'pending', 'modified', 'expired', + 'callables', 'key', 'parents', 'load_options', 'mutable_dict', + 'class_', ) if k in self.__dict__ ) if self.load_path: @@ -148,12 +148,20 @@ class InstanceState(object): def __setstate__(self, state): from sqlalchemy.orm import instrumentation - self.obj = weakref.ref(state['instance'], self._cleanup) - self.class_ = state['instance'].__class__ + inst = state['instance'] + if inst is not None: + self.obj = weakref.ref(inst, self._cleanup) + self.class_ = inst.__class__ + else: + # None being possible here generally new as of 0.7.4 + # due to storage of state in "parents". "class_" + # also new. + self.obj = None + self.class_ = state['class_'] self.manager = manager = instrumentation.manager_of_class(self.class_) if manager is None: raise orm_exc.UnmappedInstanceError( - state['instance'], + inst, "Cannot deserialize object of type %r - no mapper() has" " been configured for this class within the current Python process!" % self.class_) @@ -168,7 +176,7 @@ class InstanceState(object): self.callables = state.get('callables', {}) if self.modified: - self._strong_obj = state['instance'] + self._strong_obj = inst self.__dict__.update([ (k, state[k]) for k in ( @@ -220,13 +228,15 @@ class InstanceState(object): self.modified = False - pending = self.__dict__.get('pending', None) - mutable_dict = self.mutable_dict self.committed_state.clear() - if mutable_dict: - mutable_dict.clear() - if pending: - pending.clear() + + self.__dict__.pop('pending', None) + self.__dict__.pop('mutable_dict', None) + + # clear out 'parents' collection. not + # entirely clear how we can best determine + # which to remove, or not. + self.__dict__.pop('parents', None) for key in self.manager: impl = self.manager[key].impl diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index fa722b725..059041dd8 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -77,7 +77,7 @@ def _register_attribute(strategy, mapper, useobject, compare_function=compare_function, useobject=useobject, extension=attribute_ext, - trackparent=useobject, + trackparent=useobject and (prop.single_parent or prop.direction is interfaces.ONETOMANY), typecallable=typecallable, callable_=callable_, active_history=active_history, @@ -1308,7 +1308,7 @@ class LoadEagerFromAliasOption(PropertyOption): def single_parent_validator(desc, prop): def _do_check(state, value, oldvalue, initiator): - if value is not None: + if value is not None and initiator.key == prop.key: hasparent = initiator.hasparent(attributes.instance_state(value)) if hasparent and oldvalue is not value: raise sa_exc.InvalidRequestError( |
