diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-07-29 14:06:43 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-07-29 14:06:43 -0400 |
commit | 83326bf44c590a3b22ddf9bf658f509d1491bc0f (patch) | |
tree | 2f67a8167450d44753eb9cc2b2b3044decd3354d | |
parent | 405c223ae50e78dacac08783c414619db20df0b7 (diff) | |
download | sqlalchemy-83326bf44c590a3b22ddf9bf658f509d1491bc0f.tar.gz |
- The exception wrapping system for DBAPI errors can now accommodate
non-standard DBAPI exceptions, such as the psycopg2
TransactionRollbackError. These exceptions will now be raised
using the closest available subclass in ``sqlalchemy.exc``, in the
case of TransactionRollbackError, ``sqlalchemy.exc.OperationalError``.
fixes #3075
-rw-r--r-- | doc/build/changelog/changelog_09.rst | 11 | ||||
-rw-r--r-- | lib/sqlalchemy/exc.py | 9 | ||||
-rw-r--r-- | test/dialect/postgresql/test_dialect.py | 10 | ||||
-rw-r--r-- | test/engine/test_execute.py | 27 |
4 files changed, 53 insertions, 4 deletions
diff --git a/doc/build/changelog/changelog_09.rst b/doc/build/changelog/changelog_09.rst index c63ed7fbb..afc51f240 100644 --- a/doc/build/changelog/changelog_09.rst +++ b/doc/build/changelog/changelog_09.rst @@ -14,6 +14,17 @@ :version: 0.9.8 .. change:: + :tags: bug, postgresql + :versions: 1.0.0 + :tickets: 3075 + + The exception wrapping system for DBAPI errors can now accommodate + non-standard DBAPI exceptions, such as the psycopg2 + TransactionRollbackError. These exceptions will now be raised + using the closest available subclass in ``sqlalchemy.exc``, in the + case of TransactionRollbackError, ``sqlalchemy.exc.OperationalError``. + + .. change:: :tags: bug, sql :versions: 1.0.0 :tickets: 3144, 3067 diff --git a/lib/sqlalchemy/exc.py b/lib/sqlalchemy/exc.py index 7d333fc01..a82bae33f 100644 --- a/lib/sqlalchemy/exc.py +++ b/lib/sqlalchemy/exc.py @@ -294,9 +294,12 @@ class DBAPIError(StatementError): statement, params, orig ) - name, glob = orig.__class__.__name__, globals() - if name in glob and issubclass(glob[name], DBAPIError): - cls = glob[name] + glob = globals() + for super_ in orig.__class__.__mro__: + name = super_.__name__ + if name in glob and issubclass(glob[name], DBAPIError): + cls = glob[name] + break return cls(statement, params, orig, connection_invalidated) diff --git a/test/dialect/postgresql/test_dialect.py b/test/dialect/postgresql/test_dialect.py index 11b277b66..b751bbcdd 100644 --- a/test/dialect/postgresql/test_dialect.py +++ b/test/dialect/postgresql/test_dialect.py @@ -65,6 +65,16 @@ class MiscTest(fixtures.TestBase, AssertsExecutionResults, AssertsCompiledSQL): assert testing.db.dialect.dbapi.__version__.\ startswith(".".join(str(x) for x in v)) + @testing.only_on('postgresql+psycopg2', 'psycopg2-specific feature') + def test_psycopg2_non_standard_err(self): + from psycopg2.extensions import TransactionRollbackError + import psycopg2 + + exception = exc.DBAPIError.instance( + "some statement", {}, TransactionRollbackError("foo"), + psycopg2.Error) + assert isinstance(exception, exc.OperationalError) + # currently not passing with pg 9.3 that does not seem to generate # any notices here, would rather find a way to mock this @testing.requires.no_coverage diff --git a/test/engine/test_execute.py b/test/engine/test_execute.py index efce371a5..36f3fbf72 100644 --- a/test/engine/test_execute.py +++ b/test/engine/test_execute.py @@ -20,7 +20,7 @@ from sqlalchemy.engine import result as _result, default from sqlalchemy.engine.base import Engine from sqlalchemy.testing import fixtures from sqlalchemy.testing.mock import Mock, call, patch -from contextlib import contextmanager +from contextlib import contextmanager, nested users, metadata, users_autoinc = None, None, None class ExecuteTest(fixtures.TestBase): @@ -236,6 +236,31 @@ class ExecuteTest(fixtures.TestBase): ) eq_(is_disconnect.call_count, 0) + def test_exception_wrapping_non_standard_dbapi_error(self): + class DBAPIError(Exception): + pass + + class OperationalError(DBAPIError): + pass + + class NonStandardException(OperationalError): + pass + + with nested( + patch.object(testing.db.dialect, "dbapi", Mock(Error=DBAPIError)), + patch.object( + testing.db.dialect, "is_disconnect", + lambda *arg: False), + patch.object( + testing.db.dialect, "do_execute", + Mock(side_effect=NonStandardException)), + ): + with testing.db.connect() as conn: + assert_raises( + tsa.exc.OperationalError, + conn.execute, "select 1" + ) + def test_exception_wrapping_non_dbapi_statement(self): class MyType(TypeDecorator): |