summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/sqlalchemy/orm/attributes.py108
-rw-r--r--lib/sqlalchemy/orm/dynamic.py10
-rw-r--r--lib/sqlalchemy/orm/exc.py8
-rw-r--r--lib/sqlalchemy/orm/state.py36
-rw-r--r--lib/sqlalchemy/orm/strategies.py4
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(