diff options
| -rw-r--r-- | doc/build/changelog/changelog_11.rst | 12 | ||||
| -rw-r--r-- | lib/sqlalchemy/engine/base.py | 3 | ||||
| -rw-r--r-- | lib/sqlalchemy/testing/assertions.py | 9 | ||||
| -rw-r--r-- | lib/sqlalchemy/util/langhelpers.py | 8 | ||||
| -rw-r--r-- | test/engine/test_execute.py | 22 | ||||
| -rw-r--r-- | test/engine/test_reconnect.py | 61 |
6 files changed, 88 insertions, 27 deletions
diff --git a/doc/build/changelog/changelog_11.rst b/doc/build/changelog/changelog_11.rst index c60550923..69bdd3274 100644 --- a/doc/build/changelog/changelog_11.rst +++ b/doc/build/changelog/changelog_11.rst @@ -22,6 +22,18 @@ :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 :versions: 1.2.0b1 diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 0334d2d7a..f680edada 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -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) diff --git a/lib/sqlalchemy/testing/assertions.py b/lib/sqlalchemy/testing/assertions.py index 0244f18a9..884556345 100644 --- a/lib/sqlalchemy/testing/assertions.py +++ b/lib/sqlalchemy/testing/assertions.py @@ -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) diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py index 41fed882d..9ca19f138 100644 --- a/lib/sqlalchemy/util/langhelpers.py +++ b/lib/sqlalchemy/util/langhelpers.py @@ -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 diff --git a/test/engine/test_execute.py b/test/engine/test_execute.py index 54a85bf9f..eff1026cd 100644 --- a/test/engine/test_execute.py +++ b/test/engine/test_execute.py @@ -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 @@ -1834,6 +1834,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 diff --git a/test/engine/test_reconnect.py b/test/engine/test_reconnect.py index be60056a5..f798ff845 100644 --- a/test/engine/test_reconnect.py +++ b/test/engine/test_reconnect.py @@ -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) @@ -408,11 +409,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 @@ -433,11 +440,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 @@ -448,11 +460,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 @@ -765,10 +782,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() |
