summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2014-07-29 14:06:43 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2014-07-29 14:06:43 -0400
commit83326bf44c590a3b22ddf9bf658f509d1491bc0f (patch)
tree2f67a8167450d44753eb9cc2b2b3044decd3354d
parent405c223ae50e78dacac08783c414619db20df0b7 (diff)
downloadsqlalchemy-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.rst11
-rw-r--r--lib/sqlalchemy/exc.py9
-rw-r--r--test/dialect/postgresql/test_dialect.py10
-rw-r--r--test/engine/test_execute.py27
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):