]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Add safe_reraise() + warnings only to Connection._autorollback
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 27 Mar 2017 14:52:58 +0000 (10:52 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 27 Mar 2017 15:15:41 +0000 (11:15 -0400)
Added an exception handler that will warn for the "cause" exception on
Py2K when the "autorollback" feature of :class:`.Connection` itself
raises an exception. In Py3K, the two exceptions are naturally reported
by the interpreter as one occurring during the handling of the other.
This is continuing with the series of changes for rollback failure
handling that were last visited as part of :ticket:`2696` in 1.0.12.

Change-Id: I600ba455a14ebaea27c6189889181f97c632f179
Fixes: #3946
(cherry picked from commit c0a224aba3d4e2a41f92a29f9d18c6cb9d09d61f)

doc/build/changelog/changelog_11.rst
lib/sqlalchemy/engine/base.py
lib/sqlalchemy/testing/assertions.py
lib/sqlalchemy/util/langhelpers.py
test/engine/test_execute.py
test/engine/test_reconnect.py

index c60550923d838e20655b761faa525aeb713ea971..69bdd32742cbb6b6fcaf768ff1fe174626b2baf4 100644 (file)
 .. changelog::
     :version: 1.1.7
 
+    .. change::
+        :tags: bug, engine
+        :tickets: 3946
+        :versions: 1.2.0b1
+
+        Added an exception handler that will warn for the "cause" exception on
+        Py2K when the "autorollback" feature of :class:`.Connection` itself
+        raises an exception. In Py3K, the two exceptions are naturally reported
+        by the interpreter as one occurring during the handling of the other.
+        This is continuing with the series of changes for rollback failure
+        handling that were last visited as part of :ticket:`2696` in 1.0.12.
+
     .. change::
         :tags: bug, orm
         :tickets: 3947
index 0334d2d7a8ac189434f1a2c9848a09164780de93..f680edadaa5825c67d1458bc8d15c70e5a4695aa 100644 (file)
@@ -1383,7 +1383,8 @@ class Connection(Connectable):
             if not self._is_disconnect:
                 if cursor:
                     self._safe_close_cursor(cursor)
-                self._autorollback()
+                with util.safe_reraise(warn_only=True):
+                    self._autorollback()
 
             if newraise:
                 util.raise_from_cause(newraise, exc_info)
index 44551bd863c61847534f240e690ce73544d99e48..3ee38937f746cee88db4bdb684e9fd984b950c5a 100644 (file)
@@ -10,11 +10,11 @@ from __future__ import absolute_import
 from . import util as testutil
 from sqlalchemy import pool, orm, util
 from sqlalchemy.engine import default, url
-from sqlalchemy.util import decorator
+from sqlalchemy.util import decorator, compat
 from sqlalchemy import types as sqltypes, schema, exc as sa_exc
 import warnings
 import re
-from .exclusions import db_spec, _is_excluded
+from .exclusions import db_spec
 from . import assertsql
 from . import config
 from .util import fail
@@ -118,7 +118,8 @@ def uses_deprecated(*messages):
 
 
 @contextlib.contextmanager
-def _expect_warnings(exc_cls, messages, regex=True, assert_=True):
+def _expect_warnings(exc_cls, messages, regex=True, assert_=True,
+                     py2konly=False):
 
     if regex:
         filters = [re.compile(msg, re.I | re.S) for msg in messages]
@@ -147,7 +148,7 @@ def _expect_warnings(exc_cls, messages, regex=True, assert_=True):
     with mock.patch("warnings.warn", our_warn):
         yield
 
-    if assert_:
+    if assert_ and (not py2konly or not compat.py3k):
         assert not seen, "Warnings were not seen: %s" % \
             ", ".join("%r" % (s.pattern if regex else s) for s in seen)
 
index 68c0f885b58c4c1659d90eb2f29687b19d9488b3..66a06c5e9df6508ca28c6d2632d9a33559e7de38 100644 (file)
@@ -49,6 +49,11 @@ class safe_reraise(object):
 
     """
 
+    __slots__ = ('warn_only', '_exc_info')
+
+    def __init__(self, warn_only=False):
+        self.warn_only = warn_only
+
     def __enter__(self):
         self._exc_info = sys.exc_info()
 
@@ -57,7 +62,8 @@ class safe_reraise(object):
         if type_ is None:
             exc_type, exc_value, exc_tb = self._exc_info
             self._exc_info = None   # remove potential circular references
-            compat.reraise(exc_type, exc_value, exc_tb)
+            if not self.warn_only:
+                compat.reraise(exc_type, exc_value, exc_tb)
         else:
             if not compat.py3k and self._exc_info and self._exc_info[1]:
                 # emulate Py3K's behavior of telling us when an exception
index 32834f950b8b246211d14ff911a6f7d717d6261b..4cc101598beafccd1a78c9de4d2c304e6b1f4ee8 100644 (file)
@@ -1,7 +1,7 @@
 # coding: utf-8
 
 from sqlalchemy.testing import eq_, assert_raises, assert_raises_message, \
-    config, is_, is_not_, le_
+    config, is_, is_not_, le_, expect_warnings
 import re
 from sqlalchemy.testing.util import picklers
 from sqlalchemy.interfaces import ConnectionProxy
@@ -1804,6 +1804,26 @@ class HandleErrorTest(fixtures.TestBase):
             )
             eq_(patched.call_count, 1)
 
+    def test_exception_autorollback_fails(self):
+        engine = engines.testing_engine()
+        conn = engine.connect()
+
+        def boom(connection):
+            raise engine.dialect.dbapi.OperationalError("rollback failed")
+
+        with expect_warnings(
+            r"An exception has occurred during handling of a previous "
+            r"exception.  The previous exception is.*i_dont_exist",
+            py2konly=True
+        ):
+            with patch.object(conn.dialect, "do_rollback", boom) as patched:
+                assert_raises_message(
+                    tsa.exc.OperationalError,
+                    "rollback failed",
+                    conn.execute,
+                    "insert into i_dont_exist (x) values ('y')"
+                )
+
     def test_exception_event_ad_hoc_context(self):
         """test that handle_error is called with a context in
         cases where _handle_dbapi_error() is normally called without
index f04311790df6f275774f04ebc6d228c1f0a7c9fc..db70436e066dd775d2331d2c0de2e0c591326b7b 100644 (file)
@@ -1,4 +1,5 @@
-from sqlalchemy.testing import eq_, ne_, assert_raises, assert_raises_message
+from sqlalchemy.testing import eq_, ne_, assert_raises, \
+    expect_warnings, assert_raises_message
 import time
 from sqlalchemy import (
     select, MetaData, Integer, String, create_engine, pool, exc, util)
@@ -308,11 +309,17 @@ class MockReconnectTest(fixtures.TestBase):
         self.dbapi.shutdown("rollback_no_disconnect")
 
         # raises error
-        assert_raises_message(
-            tsa.exc.DBAPIError,
-            "something broke on rollback but we didn't lose the connection",
-            conn.execute, select([1])
-        )
+        with expect_warnings(
+            "An exception has occurred during handling .*"
+            "something broke on execute but we didn't lose the connection",
+            py2konly=True
+        ):
+            assert_raises_message(
+                tsa.exc.DBAPIError,
+                "something broke on rollback but we didn't "
+                "lose the connection",
+                conn.execute, select([1])
+            )
 
         assert conn.closed
         assert not conn.invalidated
@@ -333,11 +340,16 @@ class MockReconnectTest(fixtures.TestBase):
         self.dbapi.shutdown("rollback")
 
         # raises error
-        assert_raises_message(
-            tsa.exc.DBAPIError,
-            "Lost the DB connection on rollback",
-            conn.execute, select([1])
-        )
+        with expect_warnings(
+            "An exception has occurred during handling .*"
+            "something broke on execute but we didn't lose the connection",
+            py2konly=True
+        ):
+            assert_raises_message(
+                tsa.exc.DBAPIError,
+                "Lost the DB connection on rollback",
+                conn.execute, select([1])
+            )
 
         assert not conn.closed
         assert conn.invalidated
@@ -348,11 +360,16 @@ class MockReconnectTest(fixtures.TestBase):
         self.dbapi.shutdown("rollback")
 
         # raises error
-        assert_raises_message(
-            tsa.exc.DBAPIError,
-            "Lost the DB connection on rollback",
-            conn.execute, select([1])
-        )
+        with expect_warnings(
+            "An exception has occurred during handling .*"
+            "something broke on execute but we didn't lose the connection",
+            py2konly=True
+        ):
+            assert_raises_message(
+                tsa.exc.DBAPIError,
+                "Lost the DB connection on rollback",
+                conn.execute, select([1])
+            )
 
         assert conn.closed
         assert conn.invalidated
@@ -666,10 +683,14 @@ class RealReconnectTest(fixtures.TestBase):
         self.engine.dialect.is_disconnect = is_disconnect
         conn = self.engine.connect()
         self.engine.test_shutdown()
-        assert_raises(
-            tsa.exc.DBAPIError,
-            conn.execute, select([1])
-        )
+        with expect_warnings(
+            "An exception has occurred during handling .*",
+            py2konly=True
+        ):
+            assert_raises(
+                tsa.exc.DBAPIError,
+                conn.execute, select([1])
+            )
 
     def test_rollback_on_invalid_plain(self):
         conn = self.engine.connect()