ConcurrentModificationError in an "except:"
clause.
+ - An object that's been deleted now gets a flag
+ 'deleted', which prohibits the object from
+ being re-add()ed to the session, as previously
+ the object would live in the identity map
+ silently until its attributes were accessed.
+ The make_transient() function now resets this
+ flag along with the "key" flag.
+
+ - make_transient() can be safely called on an
+ already transient instance.
+
- a warning is emitted in mapper() if the polymorphic_on
column is not present either in direct or derived
form in the mapped selectable or in the
for s in set(self._new).union(self.session._new):
self.session._expunge_state(s)
-
+
for s in set(self._deleted).union(self.session._deleted):
+ if s.deleted:
+ # assert s in self._deleted
+ del s.deleted
self.session._update_impl(s)
assert not self.session._deleted
self.identity_map.discard(state)
self._deleted.pop(state, None)
+ state.deleted = True
def _save_without_cascade(self, instance):
"""Used by scoping.py to save on init without cascade."""
raise sa_exc.InvalidRequestError(
"Instance '%s' is not persisted" %
mapperutil.state_str(state))
-
+
+ if state.deleted:
+ raise sa_exc.InvalidRequestError(
+ "Instance '%s' has been deleted. Use the make_transient() "
+ "function to send this object back to the transient state." %
+ mapperutil.state_str(state)
+ )
self._attach(state)
self._deleted.pop(state, None)
self.identity_map.add(state)
This will remove its association with any
session and additionally will remove its "identity key",
such that it's as though the object were newly constructed,
- except retaining its values.
+ except retaining its values. It also resets the
+ "deleted" flag on the state if this object
+ had been explicitly deleted by its session.
Attributes which were "expired" or deferred at the
instance level are reverted to undefined, and
# remove expired state and
# deferred callables
state.callables.clear()
- del state.key
-
+ if state.key:
+ del state.key
+ if state.deleted:
+ del state.deleted
def object_session(instance):
"""Return the ``Session`` to which instance belongs.
assert u1.id is None
assert u1.name is None
+ # works twice
+ make_transient(u1)
+
+ sess.close()
+
+ u1.name = 'test2'
+ sess.add(u1)
+ sess.flush()
+ assert u1 in sess
+ sess.delete(u1)
+ sess.flush()
+ assert u1 not in sess
+
+ assert_raises(sa.exc.InvalidRequestError, sess.add, u1)
+ make_transient(u1)
+ sess.add(u1)
+ sess.flush()
+ assert u1 in sess
+
+ @testing.resolve_artifact_names
+ def test_deleted_flag(self):
+ mapper(User, users)
+
+ sess = sessionmaker()()
+
+ u1 = User(name='u1')
+ sess.add(u1)
+ sess.commit()
+
+ sess.delete(u1)
+ sess.flush()
+ assert u1 not in sess
+ assert_raises(sa.exc.InvalidRequestError, sess.add, u1)
+ sess.rollback()
+ assert u1 in sess
+
+ sess.delete(u1)
+ sess.commit()
+ assert u1 not in sess
+ assert_raises(sa.exc.InvalidRequestError, sess.add, u1)
+
+ make_transient(u1)
+ sess.add(u1)
+ sess.commit()
+
+ eq_(sess.query(User).count(), 1)
@testing.resolve_artifact_names
def test_autoflush_expressions(self):