]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Implement an error lookup
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 8 Dec 2017 23:08:40 +0000 (18:08 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 27 Dec 2017 15:33:22 +0000 (10:33 -0500)
Add codes to commonly raised error messages and classes
that link back to fixed documentation sections
giving background on these messages.

Change-Id: I78d0660add7026bb662e20305a59283b20616954

doc/build/changelog/unreleased_12/errlookup.rst [new file with mode: 0644]
lib/sqlalchemy/exc.py
lib/sqlalchemy/orm/exc.py
lib/sqlalchemy/pool.py
lib/sqlalchemy/sql/compiler.py
test/base/test_except.py

diff --git a/doc/build/changelog/unreleased_12/errlookup.rst b/doc/build/changelog/unreleased_12/errlookup.rst
new file mode 100644 (file)
index 0000000..9348db3
--- /dev/null
@@ -0,0 +1,7 @@
+.. change::
+    :tags: feature, misc
+
+    Added a new errors section to the documentation with background
+    about common error messages.   Selected exceptions within SQLAlchemy
+    will include a link in their string output to the relevant section
+    within this page.
index 3db3c1085df0c80fe1d333593a40bfca04d1a35e..9a5dc015915f80e8e70d4ddf7bd6ff7cba2c8694 100644 (file)
@@ -13,10 +13,50 @@ raised as a result of DBAPI exceptions are all subclasses of
 
 """
 
+from .util import compat
+
 
 class SQLAlchemyError(Exception):
     """Generic error class."""
 
+    code = None
+
+    def __init__(self, *arg, **kw):
+        code = kw.pop('code', None)
+        if code is not None:
+            self.code = code
+        super(SQLAlchemyError, self).__init__(*arg, **kw)
+
+    def _code_str(self):
+        if not self.code:
+            return ""
+        else:
+            return (
+                "(Background on this error at: "
+                "http://sqlalche.me/e/%s)" % (self.code, )
+            )
+
+    def _message(self):
+        # get string representation just like Exception.__str__(self),
+        # but also support if the string has non-ascii chars
+        if len(self.args) == 1:
+            return compat.text_type(self.args[0])
+        else:
+            return compat.text_type(self.args)
+
+    def __str__(self):
+        message = self._message()
+
+        if self.code:
+            message = (
+                "%s %s" % (message, self._code_str())
+            )
+
+        return message
+
+    def __unicode__(self):
+        return self.__str__()
+
 
 class ArgumentError(SQLAlchemyError):
     """Raised when an invalid or conflicting function argument is supplied.
@@ -72,12 +112,12 @@ class CircularDependencyError(SQLAlchemyError):
       see :ref:`use_alter`.
 
     """
-    def __init__(self, message, cycles, edges, msg=None):
+    def __init__(self, message, cycles, edges, msg=None, code=None):
         if msg is None:
             message += " (%s)" % ", ".join(repr(s) for s in cycles)
         else:
             message = msg
-        SQLAlchemyError.__init__(self, message)
+        SQLAlchemyError.__init__(self, message, code=code)
         self.cycles = cycles
         self.edges = edges
 
@@ -137,6 +177,7 @@ class InvalidatePoolError(DisconnectionError):
     """
     invalidate_pool = True
 
+
 class TimeoutError(SQLAlchemyError):
     """Raised when a connection pool times out on getting a connection."""
 
@@ -258,8 +299,8 @@ class StatementError(SQLAlchemyError):
     orig = None
     """The DBAPI exception object."""
 
-    def __init__(self, message, statement, params, orig):
-        SQLAlchemyError.__init__(self, message)
+    def __init__(self, message, statement, params, orig, code=None):
+        SQLAlchemyError.__init__(self, message, code=code)
         self.statement = statement
         self.params = params
         self.orig = orig
@@ -275,19 +316,19 @@ class StatementError(SQLAlchemyError):
     def __str__(self):
         from sqlalchemy.sql import util
 
-        details = [SQLAlchemyError.__str__(self)]
+        details = [self._message()]
         if self.statement:
             details.append("[SQL: %r]" % self.statement)
             if self.params:
                 params_repr = util._repr_params(self.params, 10)
                 details.append("[parameters: %r]" % params_repr)
+        code_str = self._code_str()
+        if code_str:
+            details.append(code_str)
         return ' '.join([
             "(%s)" % det for det in self.detail
         ] + details)
 
-    def __unicode__(self):
-        return self.__str__()
-
 
 class DBAPIError(StatementError):
     """Raised when the execution of a database operation fails.
@@ -312,6 +353,8 @@ class DBAPIError(StatementError):
 
     """
 
+    code = 'dbapi'
+
     @classmethod
     def instance(cls, statement, params,
                  orig, dbapi_base_err,
@@ -327,7 +370,14 @@ class DBAPIError(StatementError):
         if orig is not None:
             # not a DBAPI error, statement is present.
             # raise a StatementError
-            if not isinstance(orig, dbapi_base_err) and statement:
+            if isinstance(orig, SQLAlchemyError) and statement:
+                return StatementError(
+                    "(%s.%s) %s" %
+                    (orig.__class__.__module__, orig.__class__.__name__,
+                     orig.args[0]),
+                    statement, params, orig, code=orig.code
+                )
+            elif not isinstance(orig, dbapi_base_err) and statement:
                 return StatementError(
                     "(%s.%s) %s" %
                     (orig.__class__.__module__, orig.__class__.__name__,
@@ -345,13 +395,15 @@ class DBAPIError(StatementError):
                     cls = glob[name]
                     break
 
-        return cls(statement, params, orig, connection_invalidated)
+        return cls(statement, params, orig, connection_invalidated,
+                   code=cls.code)
 
     def __reduce__(self):
         return self.__class__, (self.statement, self.params,
                                 self.orig, self.connection_invalidated)
 
-    def __init__(self, statement, params, orig, connection_invalidated=False):
+    def __init__(self, statement, params, orig, connection_invalidated=False,
+                 code=None):
         try:
             text = str(orig)
         except Exception as e:
@@ -362,7 +414,7 @@ class DBAPIError(StatementError):
                 orig.__class__.__module__, orig.__class__.__name__, text, ),
             statement,
             params,
-            orig
+            orig, code=code
         )
         self.connection_invalidated = connection_invalidated
 
@@ -370,34 +422,49 @@ class DBAPIError(StatementError):
 class InterfaceError(DBAPIError):
     """Wraps a DB-API InterfaceError."""
 
+    code = "rvf5"
+
 
 class DatabaseError(DBAPIError):
     """Wraps a DB-API DatabaseError."""
 
+    code = "4xp6"
+
 
 class DataError(DatabaseError):
     """Wraps a DB-API DataError."""
 
+    code = "9h9h"
+
 
 class OperationalError(DatabaseError):
     """Wraps a DB-API OperationalError."""
 
+    code = "e3q8"
+
 
 class IntegrityError(DatabaseError):
     """Wraps a DB-API IntegrityError."""
 
+    code = "gkpj"
+
 
 class InternalError(DatabaseError):
     """Wraps a DB-API InternalError."""
 
+    code = "2j85"
+
 
 class ProgrammingError(DatabaseError):
     """Wraps a DB-API ProgrammingError."""
 
+    code = "f405"
+
 
 class NotSupportedError(DatabaseError):
     """Wraps a DB-API NotSupportedError."""
 
+    code = "tw8g"
 
 # Warnings
 
index bd63a1b5a244687f81427463e3ae133ab7d7e4a6..41624c38d0ed3f73a443f6de88b9849739c2af75 100644 (file)
@@ -60,6 +60,8 @@ class DetachedInstanceError(sa_exc.SQLAlchemyError):
     """An attempt to access unloaded attributes on a
     mapped instance that is detached."""
 
+    code = "bhk3"
+
 
 class UnmappedInstanceError(UnmappedError):
     """An mapping operation was requested for an unknown instance."""
index ef49ffd6008314d0d1dcd7494d15dc96d98752e2..71d090839e8f499a3fcaffc609faba323d3708ab 100644 (file)
@@ -1176,7 +1176,7 @@ class QueuePool(Pool):
                 raise exc.TimeoutError(
                     "QueuePool limit of size %d overflow %d reached, "
                     "connection timed out, timeout %d" %
-                    (self.size(), self.overflow(), self._timeout))
+                    (self.size(), self.overflow(), self._timeout), code="3o7r")
 
         if self._inc_overflow():
             try:
index 9ed75ca06f4bfd60c00aaa78898b71d3c4797111..cb058affa28a4c5bb0dbd1f586d0e010ccdda1ef 100644 (file)
@@ -271,7 +271,7 @@ class Compiled(object):
         if e is None:
             raise exc.UnboundExecutionError(
                 "This Compiled object is not bound to any Engine "
-                "or Connection.")
+                "or Connection.", code="2afi")
         return e._execute_compiled(self, multiparams, params)
 
     def scalar(self, *multiparams, **params):
@@ -540,11 +540,11 @@ class SQLCompiler(Compiled):
                         raise exc.InvalidRequestError(
                             "A value is required for bind parameter %r, "
                             "in parameter group %d" %
-                            (bindparam.key, _group_number))
+                            (bindparam.key, _group_number), code="cd3x")
                     else:
                         raise exc.InvalidRequestError(
                             "A value is required for bind parameter %r"
-                            % bindparam.key)
+                            % bindparam.key, code="cd3x")
 
                 elif bindparam.callable:
                     pd[name] = bindparam.effective_value
@@ -559,11 +559,11 @@ class SQLCompiler(Compiled):
                         raise exc.InvalidRequestError(
                             "A value is required for bind parameter %r, "
                             "in parameter group %d" %
-                            (bindparam.key, _group_number))
+                            (bindparam.key, _group_number), code="cd3x")
                     else:
                         raise exc.InvalidRequestError(
                             "A value is required for bind parameter %r"
-                            % bindparam.key)
+                            % bindparam.key, code="cd3x")
 
                 if bindparam.callable:
                     pd[self.bind_names[bindparam]] = bindparam.effective_value
index ffbe6c01b6272de7b40a48da78db8f6a5fcf5d5e..ce8655af0b7340a9c6a8b97e3c5432c8adcf8884 100644 (file)
@@ -1,3 +1,5 @@
+#! coding:utf-8
+
 """Tests exceptions and DB-API exception wrapping."""
 
 
@@ -5,6 +7,7 @@ from sqlalchemy import exc as sa_exceptions
 from sqlalchemy.testing import fixtures
 from sqlalchemy.testing import eq_
 from sqlalchemy.engine import default
+from sqlalchemy.util import u, compat
 
 
 class Error(Exception):
@@ -69,7 +72,58 @@ class WrapTest(fixtures.TestBase):
             eq_(
                 str(exc),
                 "(test.base.test_except.OperationalError)  "
-                "[SQL: 'this is a message']")
+                "[SQL: 'this is a message'] (Background on this error at: "
+                "http://sqlalche.me/e/e3q8)")
+
+    def test_statement_error_no_code(self):
+        try:
+            raise sa_exceptions.DBAPIError.instance(
+                'select * from table', [{"x": 1}],
+                sa_exceptions.InvalidRequestError("hello"), DatabaseError)
+        except sa_exceptions.StatementError as err:
+            eq_(
+                str(err),
+                "(sqlalchemy.exc.InvalidRequestError) hello "
+                "[SQL: 'select * from table'] [parameters: [{'x': 1}]]"
+            )
+            eq_(err.args, ("(sqlalchemy.exc.InvalidRequestError) hello", ))
+
+    def test_statement_error_w_code(self):
+        try:
+            raise sa_exceptions.DBAPIError.instance(
+                'select * from table', [{"x": 1}],
+                sa_exceptions.InvalidRequestError("hello", code="abcd"),
+                DatabaseError)
+        except sa_exceptions.StatementError as err:
+            eq_(
+                str(err),
+                "(sqlalchemy.exc.InvalidRequestError) hello "
+                "[SQL: 'select * from table'] [parameters: [{'x': 1}]] "
+                "(Background on this error at: http://sqlalche.me/e/abcd)"
+            )
+            eq_(err.args, ("(sqlalchemy.exc.InvalidRequestError) hello", ))
+
+    def test_wrap_multi_arg(self):
+        # this is not supported by the API but oslo_db is doing it
+        orig = sa_exceptions.DBAPIError(False, False, False)
+        orig.args = [2006, 'Test raise operational error']
+        eq_(
+            str(orig),
+            "(2006, 'Test raise operational error') "
+            "(Background on this error at: http://sqlalche.me/e/dbapi)"
+        )
+
+    def test_wrap_unicode_arg(self):
+        # this is not supported by the API but oslo_db is doing it
+        orig = sa_exceptions.DBAPIError(False, False, False)
+        orig.args = [u('méil')]
+        eq_(
+            compat.text_type(orig),
+            compat.u(
+                "méil (Background on this error at: "
+                "http://sqlalche.me/e/dbapi)")
+        )
+        eq_(orig.args, (u('méil'),))
 
     def test_tostring_large_dict(self):
         try:
@@ -103,14 +157,19 @@ class WrapTest(fixtures.TestBase):
                 'this is a message',
                 [{1: 1}, {1: 1}, {1: 1}, {1: 1}, {1: 1}, {1: 1},
                  {1: 1}, {1: 1}, {1: 1}, {1: 1}, ],
-                OperationalError(), DatabaseError)
+                OperationalError("sql error"), DatabaseError)
         except sa_exceptions.DBAPIError as exc:
             eq_(
                 str(exc),
-                "(test.base.test_except.OperationalError)  "
+                "(test.base.test_except.OperationalError) sql error "
                 "[SQL: 'this is a message'] [parameters: [{1: 1}, "
                 "{1: 1}, {1: 1}, {1: 1}, {1: 1}, {1: 1}, {1: 1}, {1: "
-                "1}, {1: 1}, {1: 1}]]"
+                "1}, {1: 1}, {1: 1}]] (Background on this error at: "
+                "http://sqlalche.me/e/e3q8)"
+            )
+            eq_(
+                exc.args,
+                ("(test.base.test_except.OperationalError) sql error", )
             )
         try:
             raise sa_exceptions.DBAPIError.instance('this is a message', [
@@ -123,7 +182,8 @@ class WrapTest(fixtures.TestBase):
                 "[SQL: 'this is a message'] [parameters: [{1: 1}, "
                 "{1: 1}, {1: 1}, {1: 1}, {1: 1}, {1: 1}, "
                 "{1: 1}, {1: 1}  ... displaying 10 of 11 total "
-                "bound parameter sets ...  {1: 1}, {1: 1}]]"
+                "bound parameter sets ...  {1: 1}, {1: 1}]] "
+                "(Background on this error at: http://sqlalche.me/e/e3q8)"
                 )
         try:
             raise sa_exceptions.DBAPIError.instance(
@@ -138,7 +198,8 @@ class WrapTest(fixtures.TestBase):
                 str(exc),
                 "(test.base.test_except.OperationalError)  "
                 "[SQL: 'this is a message'] [parameters: [(1,), "
-                "(1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,)]]")
+                "(1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,)]] "
+                "(Background on this error at: http://sqlalche.me/e/e3q8)")
         try:
             raise sa_exceptions.DBAPIError.instance('this is a message', [
                 (1, ), (1, ), (1, ), (1, ), (1, ), (1, ), (1, ), (1, ), (1, ),
@@ -150,7 +211,8 @@ class WrapTest(fixtures.TestBase):
                 "[SQL: 'this is a message'] [parameters: [(1,), "
                 "(1,), (1,), (1,), (1,), (1,), (1,), (1,)  "
                 "... displaying 10 of 11 total bound "
-                "parameter sets ...  (1,), (1,)]]"
+                "parameter sets ...  (1,), (1,)]] "
+                "(Background on this error at: http://sqlalche.me/e/e3q8)"
                 )
 
     def test_db_error_busted_dbapi(self):