]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Implement explicit autobegin step for Session
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 2 Jan 2020 17:48:23 +0000 (12:48 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 3 Jan 2020 18:30:53 +0000 (13:30 -0500)
The :class:`.Session` object no longer initates a
:class:`.SessionTransaction` object immediately upon construction or after
the previous transaction is closed; instead, "autobegin" logic now
initiates the new :class:`.SessionTransaction` on demand when it is next
needed.  Rationale includes to remove reference cycles from a
:class:`.Session` that has been closed out, as well as to remove the
overhead incurred by the creation of :class:`.SessionTransaction` objects
that are often discarded immediately. This change affects the behavior of
the :meth:`.SessionEvents.after_transaction_create` hook in that the event
will be emitted when the :class:`.Session` first requires a
:class:`.SessionTransaction` be present, rather than whenever the
:class:`.Session` were created or the previous :class:`.SessionTransaction`
were closed.   Interactions with the :class:`.Engine` and the database
itself remain unaffected.

Fixes: #5074
Change-Id: I00b656eb5ee03d87104257a214214617aacae16c

doc/build/changelog/migration_14.rst
lib/sqlalchemy/orm/session.py
test/aaa_profiling/test_memusage.py
test/aaa_profiling/test_orm.py
test/orm/test_events.py
test/orm/test_session.py
test/orm/test_transaction.py

index a0a1f1f74222d00e69d497abc6e15936814b960d..3a57191e356dc89e47f49bfbb42c2672a91ce255 100644 (file)
@@ -398,6 +398,59 @@ as was present previously.
 Behavioral Changes - ORM
 ========================
 
+.. _change_5074:
+
+Session does not immediately create a new SessionTransaction object
+----------------------------------------------------------------------------
+
+The :class:`.Session` object's default behavior of ``autocommit=False``
+historically has meant that there is always a :class:`.SessionTransaction`
+object in play, associated with the :class:`.Session` via the
+:attr:`.Session.transaction` attribute.   When the given
+:class:`.SessionTransaction` was complete, due to a commit, rollback, or close,
+it was immediately replaced with a new one.  The :class:`.SessionTransaction`
+by itself does not imply the usage of any connection-oriented resources, so
+this long-standing behavior has a particular elegance to it in that the state
+of :attr:`.Session.transaction` is always predictable as non-None.
+
+However, as part of the initiative in :ticket:`5056` to greatly reduce
+reference cycles, this assumption means that calling upon
+:meth:`.Session.close` results in a :class:`.Session` object that still has
+reference cycles and is more expensive to clean up, not to mention that there
+is a small overhead in constructing the :class:`.SessionTransaction`
+object, which meant that there would be unnecessary overhead created
+for a :class:`.Session` that for example invoked :meth:`.Session.commit`
+and then :meth:`.Session.close`.
+
+As such, it was decided that :meth:`.Session.close` should leave the internal
+state of ``self.transaction``, now referred to internally as
+``self._transaction``, as None, and that a new :class:`.SessionTransaction`
+should only be created when needed.  For consistency and code coverage, this
+behavior was also expanded to include all the points at which "autobegin" is
+expected, not just when :meth:`.Session.close` were called.
+
+In particular, this causes a behavioral change for applications which
+subscribe to the :meth:`.SessionEvents.after_transaction_create` event hook;
+previously, this event would be emitted when the :class:`.Session` were  first
+constructed, as well as for most actions that closed the previous transaction
+and would emit :meth:`.SessionEvents.after_transaction_end`.  The new behavior
+is that :meth:`.SessionEvents.after_transaction_create` is emitted on demand,
+when the :class:`.Session` has not yet created a  new
+:class:`.SessionTransaction` object and mapped objects are associated with the
+:class:`.Session` through methods like :meth:`.Session.add` and
+:meth:`.Session.delete`, when  the :attr:`.Session.transaction` attribute is
+called upon, when the :meth:`.Session.flush` method has tasks to complete, etc.
+
+Besides the change in when the :meth:`.SessionEvents.after_transaction_create`
+event is emitted, the change should have no other user-visible impact on the
+:class:`.Session` object's behavior; the :class:`.Session` will continue to have
+the behavior that it remains usable for new operations after :meth:`.Session.close`
+is called, and the sequencing of how the :class:`.Session` interacts with the
+:class:`.Engine` and the database itself should also remain unaffected, since
+these operations were already operating in an on-demand fashion.
+
+:ticket:`5074`
+
 .. _change_1763:
 
 Eager loaders emit during unexpire operations
index 3c58b4260ebc0cf220826701b48aff2962cf9f5e..fe0640a473c24e0810b22beeb8273f4c72646dfb 100644 (file)
@@ -123,14 +123,14 @@ class SessionTransaction(object):
 
     **Life Cycle**
 
-    A :class:`.SessionTransaction` is associated with a :class:`.Session`
-    in its default mode of ``autocommit=False`` immediately, associated
-    with no database connections.  As the :class:`.Session` is called upon
-    to emit SQL on behalf of various :class:`.Engine` or :class:`.Connection`
-    objects, a corresponding :class:`.Connection` and associated
-    :class:`.Transaction` is added to a collection within the
-    :class:`.SessionTransaction` object, becoming one of the
-    connection/transaction pairs maintained by the
+    A :class:`.SessionTransaction` is associated with a :class:`.Session` in
+    its default mode of ``autocommit=False`` whenever the "autobegin" process
+    takes place, associated with no database connections.  As the
+    :class:`.Session` is called upon to emit SQL on behalf of various
+    :class:`.Engine` or :class:`.Connection` objects, a corresponding
+    :class:`.Connection` and associated :class:`.Transaction` is added to a
+    collection within the :class:`.SessionTransaction` object, becoming one of
+    the connection/transaction pairs maintained by the
     :class:`.SessionTransaction`.  The start of a :class:`.SessionTransaction`
     can be tracked using the :meth:`.SessionEvents.after_transaction_create`
     event.
@@ -140,13 +140,18 @@ class SessionTransaction(object):
     :meth:`.Session.close` methods are called.  At this point, the
     :class:`.SessionTransaction` removes its association with its parent
     :class:`.Session`.   A :class:`.Session` that is in ``autocommit=False``
-    mode will create a new :class:`.SessionTransaction` to replace it
-    immediately, whereas a :class:`.Session` that's in ``autocommit=True``
-    mode will remain without a :class:`.SessionTransaction` until the
-    :meth:`.Session.begin` method is called.  The end of a
+    mode will create a new :class:`.SessionTransaction` to replace it when the
+    next "autobegin" event occurs, whereas a :class:`.Session` that's in
+    ``autocommit=True`` mode will remain without a :class:`.SessionTransaction`
+    until the :meth:`.Session.begin` method is called.  The end of a
     :class:`.SessionTransaction` can be tracked using the
     :meth:`.SessionEvents.after_transaction_end` event.
 
+    .. versionchanged:: 1.4 the :class:`.SessionTransaction` is not created
+       immediately within a :class:`.Session` when constructed or when the
+       previous transaction is removed, it instead is created when the
+       :class:`.Session` is next used.
+
     **Nesting and Subtransactions**
 
     Another detail of :class:`.SessionTransaction` behavior is that it is
@@ -217,7 +222,7 @@ class SessionTransaction(object):
 
     _rollback_exception = None
 
-    def __init__(self, session, parent=None, nested=False):
+    def __init__(self, session, parent=None, nested=False, autobegin=False):
         self.session = session
         self._connections = {}
         self._parent = parent
@@ -230,7 +235,7 @@ class SessionTransaction(object):
             )
 
         if self.session._enable_transaction_accounting:
-            self._take_snapshot()
+            self._take_snapshot(autobegin=autobegin)
 
         self.session.dispatch.after_transaction_create(self.session, self)
 
@@ -334,7 +339,7 @@ class SessionTransaction(object):
 
         return result
 
-    def _take_snapshot(self):
+    def _take_snapshot(self, autobegin=False):
         if not self._is_transaction_boundary:
             self._new = self._parent._new
             self._deleted = self._parent._deleted
@@ -342,7 +347,7 @@ class SessionTransaction(object):
             self._key_switches = self._parent._key_switches
             return
 
-        if not self.session._flushing:
+        if not autobegin and not self.session._flushing:
             self.session.flush()
 
         self._new = weakref.WeakKeyDictionary()
@@ -577,7 +582,7 @@ class SessionTransaction(object):
         return self._parent
 
     def close(self, invalidate=False):
-        self.session.transaction = self._parent
+        self.session._transaction = self._parent
         if self._parent is None:
             for connection, transaction, autoclose in set(
                 self._connections.values()
@@ -592,9 +597,6 @@ class SessionTransaction(object):
         self._state = CLOSED
         self.session.dispatch.after_transaction_end(self.session, self)
 
-        if self._parent is None:
-            if not self.session.autocommit:
-                self.session.begin()
         self.session = None
         self._connections = None
 
@@ -603,7 +605,7 @@ class SessionTransaction(object):
 
     def __exit__(self, type_, value, traceback):
         self._assert_active(deactive_ok=True, prepared_ok=True)
-        if self.session.transaction is None:
+        if self.session._transaction is None:
             return
         if type_ is None:
             try:
@@ -832,7 +834,7 @@ class Session(_SessionClassMethods):
         self.__binds = {}
         self._flushing = False
         self._warn_on_events = False
-        self.transaction = None
+        self._transaction = None
         self.hash_key = _new_sessionid()
         self.autoflush = autoflush
         self.autocommit = autocommit
@@ -849,14 +851,24 @@ class Session(_SessionClassMethods):
             for key, bind in binds.items():
                 self._add_bind(key, bind)
 
-        if not self.autocommit:
-            self.begin()
         _sessions[self.hash_key] = self
 
     connection_callable = None
 
-    transaction = None
-    """The current active or inactive :class:`.SessionTransaction`."""
+    @property
+    def transaction(self):
+        """The current active or inactive :class:`.SessionTransaction`.
+
+        If this session is in "autobegin" mode and the transaction was not
+        begun, this accessor will implicitly begin the transaction.
+
+        .. versionchanged:: 1.4  the :attr:`.Session.transaction` attribute
+           is now a read-only descriptor that will automatically start a
+           transaction in "autobegin" mode if one is not present.
+
+        """
+        self._autobegin()
+        return self._transaction
 
     @util.memoized_property
     def info(self):
@@ -873,6 +885,13 @@ class Session(_SessionClassMethods):
         """
         return {}
 
+    def _autobegin(self):
+        if not self.autocommit and self._transaction is None:
+            self._transaction = SessionTransaction(self, autobegin=True)
+            return True
+
+        return False
+
     def begin(self, subtransactions=False, nested=False):
         """Begin a transaction on this :class:`.Session`.
 
@@ -925,17 +944,22 @@ class Session(_SessionClassMethods):
 
 
         """
-        if self.transaction is not None:
+
+        if self._autobegin():
+            if not subtransactions and not nested:
+                return
+
+        if self._transaction is not None:
             if subtransactions or nested:
-                self.transaction = self.transaction._begin(nested=nested)
+                self._transaction = self._transaction._begin(nested=nested)
             else:
                 raise sa_exc.InvalidRequestError(
                     "A transaction is already begun.  Use "
                     "subtransactions=True to allow subtransactions."
                 )
         else:
-            self.transaction = SessionTransaction(self, nested=nested)
-        return self.transaction  # needed for __enter__/__exit__ hook
+            self._transaction = SessionTransaction(self, nested=nested)
+        return self._transaction  # needed for __enter__/__exit__ hook
 
     def begin_nested(self):
         """Begin a "nested" transaction on this Session, e.g. SAVEPOINT.
@@ -977,10 +1001,10 @@ class Session(_SessionClassMethods):
             :ref:`session_rollback`
 
         """
-        if self.transaction is None:
+        if self._transaction is None:
             pass
         else:
-            self.transaction.rollback()
+            self._transaction.rollback()
 
     def commit(self):
         """Flush pending changes and commit the current transaction.
@@ -1010,13 +1034,11 @@ class Session(_SessionClassMethods):
             :ref:`session_committing`
 
         """
-        if self.transaction is None:
-            if not self.autocommit:
-                self.begin()
-            else:
+        if self._transaction is None:
+            if not self._autobegin():
                 raise sa_exc.InvalidRequestError("No transaction is begun.")
 
-        self.transaction.commit()
+        self._transaction.commit()
 
     def prepare(self):
         """Prepare the current transaction in progress for two phase commit.
@@ -1029,13 +1051,11 @@ class Session(_SessionClassMethods):
         :exc:`~sqlalchemy.exc.InvalidRequestError` is raised.
 
         """
-        if self.transaction is None:
-            if not self.autocommit:
-                self.begin()
-            else:
+        if self._transaction is None:
+            if not self._autobegin():
                 raise sa_exc.InvalidRequestError("No transaction is begun.")
 
-        self.transaction.prepare()
+        self._transaction.prepare()
 
     def connection(
         self,
@@ -1117,8 +1137,10 @@ class Session(_SessionClassMethods):
         )
 
     def _connection_for_bind(self, engine, execution_options=None, **kw):
-        if self.transaction is not None:
-            return self.transaction._connection_for_bind(
+        self._autobegin()
+
+        if self._transaction is not None:
+            return self._transaction._connection_for_bind(
                 engine, execution_options
             )
         else:
@@ -1272,8 +1294,13 @@ class Session(_SessionClassMethods):
         This clears all items and ends any transaction in progress.
 
         If this session were created with ``autocommit=False``, a new
-        transaction is immediately begun.  Note that this new transaction does
-        not use any connection resources until they are first needed.
+        transaction will be begun when the :class:`.Session` is next asked
+        to procure a database connection.
+
+        .. versionchanged:: 1.4  The :meth:`.Session.close` method does not
+           immediately create a new :class:`.SessionTransaction` object;
+           instead, the new :class:`.SessionTransaction` is created only if
+           the :class:`.Session` is used again for a database operation.
 
         """
         self._close_impl(invalidate=False)
@@ -1313,8 +1340,8 @@ class Session(_SessionClassMethods):
 
     def _close_impl(self, invalidate):
         self.expunge_all()
-        if self.transaction is not None:
-            for transaction in self.transaction._iterate_self_and_parents():
+        if self._transaction is not None:
+            for transaction in self._transaction._iterate_self_and_parents():
                 transaction.close(invalidate)
 
     def expunge_all(self):
@@ -1864,10 +1891,10 @@ class Session(_SessionClassMethods):
             elif self.identity_map.contains_state(state):
                 self.identity_map.safe_discard(state)
                 self._deleted.pop(state, None)
-            elif self.transaction:
+            elif self._transaction:
                 # state is "detached" from being deleted, but still present
                 # in the transaction snapshot
-                self.transaction._deleted.pop(state, None)
+                self._transaction._deleted.pop(state, None)
         statelib.InstanceState._detach_states(
             states, self, to_transient=to_transient
         )
@@ -1913,11 +1940,11 @@ class Session(_SessionClassMethods):
                     # state has already replaced this one in the identity
                     # map (see test/orm/test_naturalpks.py ReversePKsTest)
                     self.identity_map.safe_discard(state)
-                    if state in self.transaction._key_switches:
-                        orig_key = self.transaction._key_switches[state][0]
+                    if state in self._transaction._key_switches:
+                        orig_key = self._transaction._key_switches[state][0]
                     else:
                         orig_key = state.key
-                    self.transaction._key_switches[state] = (
+                    self._transaction._key_switches[state] = (
                         orig_key,
                         instance_key,
                     )
@@ -1955,18 +1982,18 @@ class Session(_SessionClassMethods):
             self._new.pop(state)
 
     def _register_altered(self, states):
-        if self._enable_transaction_accounting and self.transaction:
+        if self._enable_transaction_accounting and self._transaction:
             for state in states:
                 if state in self._new:
-                    self.transaction._new[state] = True
+                    self._transaction._new[state] = True
                 else:
-                    self.transaction._dirty[state] = True
+                    self._transaction._dirty[state] = True
 
     def _remove_newly_deleted(self, states):
         persistent_to_deleted = self.dispatch.persistent_to_deleted or None
         for state in states:
-            if self._enable_transaction_accounting and self.transaction:
-                self.transaction._deleted[state] = True
+            if self._enable_transaction_accounting and self._transaction:
+                self._transaction._deleted[state] = True
 
             if persistent_to_deleted is not None:
                 # get a strong reference before we pop out of
@@ -2427,6 +2454,8 @@ class Session(_SessionClassMethods):
             self._after_attach(state, obj)
 
     def _before_attach(self, state, obj):
+        self._autobegin()
+
         if state.session_id == self.hash_key:
             return False
 
@@ -3083,7 +3112,8 @@ class Session(_SessionClassMethods):
         :meth:`.SessionEvents.after_rollback` and related events.
 
         """
-        return self.transaction and self.transaction.is_active
+        self._autobegin()
+        return self._transaction and self._transaction.is_active
 
     identity_map = None
     """A mapping of object identities to objects themselves.
index 301724509543626061256beda91014b506467ffc..71d5481d317448abc1afa28f87f1a404e73f0909 100644 (file)
@@ -1457,3 +1457,48 @@ class CycleTest(_fixtures.FixtureTest):
             visit_binary_product(visit, expr)
 
         go()
+
+    def test_session_transaction(self):
+        @assert_cycles()
+        def go():
+            s = Session(testing.db)
+            s.connection()
+            s.close()
+
+        go()
+
+    def test_session_commit_rollback(self):
+        # this is enabled by #5074
+        @assert_cycles()
+        def go():
+            s = Session(testing.db)
+            s.connection()
+            s.commit()
+
+        go()
+
+        @assert_cycles()
+        def go():
+            s = Session(testing.db)
+            s.connection()
+            s.rollback()
+
+        go()
+
+    def test_session_multi_transaction(self):
+        @assert_cycles()
+        def go():
+            s = Session(testing.db)
+            assert s._transaction is None
+
+            s.connection()
+
+            s.close()
+            assert s._transaction is None
+
+            s.connection()
+            assert s._transaction is not None
+
+            s.close()
+
+        go()
index e2df85a33997db185c4647c34cdb9a3ca624b581..58c7de7edc32e8cc114efac400c5209f95ab4626 100644 (file)
@@ -89,6 +89,8 @@ class MergeTest(fixtures.MappedTest):
         # down from 185 on this this is a small slice of a usually
         # bigger operation so using a small variance
 
+        sess2.transaction  # autobegin
+
         @profiling.function_call_count(variance=0.10)
         def go1():
             return sess2.merge(p1, load=False)
@@ -97,6 +99,8 @@ class MergeTest(fixtures.MappedTest):
 
         # third call, merge object already present. almost no calls.
 
+        sess2.transaction  # autobegin
+
         @profiling.function_call_count(variance=0.10)
         def go2():
             return sess2.merge(p2, load=False)
@@ -115,6 +119,8 @@ class MergeTest(fixtures.MappedTest):
         # using sqlite3 the C extension took it back up to approx. 1257
         # (py2.6)
 
+        sess2.transaction  # autobegin
+
         @profiling.function_call_count(variance=0.10)
         def go():
             sess2.merge(p1)
index b0b140ffa5e08672a2e7339ac3e087a3c7d093a4..6957a903093bb2062dc6234408804c36a1b5019f 100644 (file)
@@ -1500,6 +1500,7 @@ class SessionEventsTest(_RemoveListeners, _fixtures.FixtureTest):
         eq_(
             canary,
             [
+                "after_transaction_create",  # changed in #5074
                 "before_attach",
                 "after_attach",
                 "before_commit",
@@ -1522,7 +1523,6 @@ class SessionEventsTest(_RemoveListeners, _fixtures.FixtureTest):
                 "after_transaction_end",
                 "after_soft_rollback",
                 "after_transaction_end",
-                "after_transaction_create",
                 "after_soft_rollback",
             ],
         )
@@ -1564,6 +1564,7 @@ class SessionEventsTest(_RemoveListeners, _fixtures.FixtureTest):
         eq_(
             canary,
             [
+                "after_transaction_create",  # changed due to #5074
                 "before_attach",
                 "after_attach",
                 "before_flush",
@@ -1599,7 +1600,7 @@ class SessionEventsTest(_RemoveListeners, _fixtures.FixtureTest):
                 "after_transaction_end",
                 "after_commit",
                 "after_transaction_end",
-                "after_transaction_create",
+                # no longer autocreates after #5074
             ],
         )
 
@@ -1647,10 +1648,10 @@ class SessionEventsTest(_RemoveListeners, _fixtures.FixtureTest):
         eq_(
             canary,
             [
+                "after_transaction_create",  # moved to top due to #5074
                 "before_commit",
                 "after_commit",
                 "after_transaction_end",
-                "after_transaction_create",
             ],
         )
 
@@ -1713,7 +1714,8 @@ class SessionEventsTest(_RemoveListeners, _fixtures.FixtureTest):
     def test_connection_emits_after_begin(self):
         sess, canary = self._listener_fixture(bind=testing.db)
         sess.connection()
-        eq_(canary, ["after_begin"])
+        # changed due to #5074
+        eq_(canary, ["after_transaction_create", "after_begin"])
         sess.close()
 
     def test_reentrant_flush(self):
index 4bc8e7691578e8a91ab71d37cd3f8f95d5befa56..a2f0a9aa1931fba0b2ae4a2e913efcfc96f556b8 100644 (file)
@@ -3,6 +3,7 @@ from sqlalchemy import event
 from sqlalchemy import ForeignKey
 from sqlalchemy import inspect
 from sqlalchemy import Integer
+from sqlalchemy import select
 from sqlalchemy import Sequence
 from sqlalchemy import String
 from sqlalchemy import testing
@@ -28,6 +29,7 @@ from sqlalchemy.testing import engines
 from sqlalchemy.testing import eq_
 from sqlalchemy.testing import fixtures
 from sqlalchemy.testing import is_
+from sqlalchemy.testing import is_not_
 from sqlalchemy.testing import is_true
 from sqlalchemy.testing import mock
 from sqlalchemy.testing import pickleable
@@ -121,6 +123,62 @@ class TransScopingTest(_fixtures.FixtureTest):
         s.close()
         c.execute("select * from users")
 
+    def test_autobegin_execute(self):
+        # test the new autobegin behavior introduced in #5074
+        s = Session(testing.db)
+
+        is_(s._transaction, None)
+
+        s.execute(select([1]))
+        is_not_(s._transaction, None)
+
+        s.commit()
+        is_(s._transaction, None)
+
+        s.execute(select([1]))
+        is_not_(s._transaction, None)
+
+        s.close()
+        is_(s._transaction, None)
+
+        s.execute(select([1]))
+        is_not_(s._transaction, None)
+
+        s.close()
+        is_(s._transaction, None)
+
+    def test_autobegin_flush(self):
+        # test the new autobegin behavior introduced in #5074
+        User, users = self.classes.User, self.tables.users
+
+        mapper(User, users)
+
+        s = Session(testing.db)
+
+        is_(s._transaction, None)
+
+        # empty flush, nothing happens
+        s.flush()
+        is_(s._transaction, None)
+
+        s.add(User(id=1, name="name"))
+        s.flush()
+        is_not_(s._transaction, None)
+        s.commit()
+        is_(s._transaction, None)
+
+    def test_autobegin_begin_method(self):
+        s = Session(testing.db)
+
+        s.begin()  # OK
+
+        assert_raises_message(
+            sa.exc.InvalidRequestError,
+            "A transaction is already begun.  Use "
+            "subtransactions=True to allow subtransactions.",
+            s.begin,
+        )
+
     @testing.requires.independent_connections
     @engines.close_open_connections
     def test_transaction(self):
@@ -406,6 +464,7 @@ class SessionStateTest(_fixtures.FixtureTest):
         sess.flush()
         assert u1 not in sess
         assert_raises(sa.exc.InvalidRequestError, sess.add, u1)
+        assert sess.transaction is not None
         sess.rollback()
         assert u1 in sess
 
index 3ad8880de2dd7be95ad9056457c387b07ffa18ba..676a37e8dedab364fde8052123df492859f09405 100644 (file)
@@ -550,6 +550,8 @@ class SessionTransactionTest(fixtures.RemovesEvents, FixtureTest):
     def test_no_sql_during_rollback(self):
         sess = create_session(bind=testing.db, autocommit=False)
 
+        sess.connection()
+
         @event.listens_for(sess, "after_rollback")
         def go(session):
             session.execute("select 1")
@@ -1273,6 +1275,7 @@ class RollbackRecoverTest(_LocalFixture):
     def test_pk_violation(self):
         User, Address = self.classes.User, self.classes.Address
         s = self.session()
+
         a1 = Address(email_address="foo")
         u1 = User(id=1, name="ed", addresses=[a1])
         s.add(u1)
@@ -1287,6 +1290,7 @@ class RollbackRecoverTest(_LocalFixture):
 
         with expect_warnings("New instance"):
             assert_raises(sa_exc.IntegrityError, s.commit)
+
         assert_raises(sa_exc.InvalidRequestError, s.commit)
         s.rollback()
         assert u2 not in s