Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Refactored database exceptions wrapping.

Squashed commit of the following:

commit 2181d83
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Wed Feb 27 14:28:39 2013 +0100

    Fixed #15901 -- Wrapped all PEP-249 exceptions.

commit 5476a5d
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Tue Feb 26 17:26:52 2013 +0100

    Added PEP 3134 exception chaining.

    Thanks Jacob Kaplan-Moss for the suggestion.

commit 9365fad
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Tue Feb 26 17:13:49 2013 +0100

    Improved API for wrapping database errors.

    Thanks Alex Gaynor for the proposal.

commit 1b463b7
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Tue Feb 26 15:00:39 2013 +0100

    Removed redundant exception wrapping.

    This is now taken care of by the cursor wrapper.

commit 524bc73
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Tue Feb 26 14:55:10 2013 +0100

    Wrapped database exceptions in the base backend.

    This covers the most common PEP-249 APIs:
    - Connection APIs: close(), commit(), rollback(), cursor()
    - Cursor APIs: callproc(), close(), execute(), executemany(),
      fetchone(), fetchmany(), fetchall(), nextset().

    Fixed #19920.

commit a66746b
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Tue Feb 26 14:53:34 2013 +0100

    Added a wrap_database_exception context manager and decorator.

    It re-throws backend-specific exceptions using Django's common wrappers.
  • Loading branch information...
commit 59a352087591a26023412cbcb830cd1d34fc9b99 1 parent 50328f0
Aymeric Augustin authored February 26, 2013
7  django/db/__init__.py
... ...
@@ -1,8 +1,11 @@
1 1
 from django.conf import settings
2 2
 from django.core import signals
3 3
 from django.core.exceptions import ImproperlyConfigured
4  
-from django.db.utils import (ConnectionHandler, ConnectionRouter,
5  
-    load_backend, DEFAULT_DB_ALIAS, DatabaseError, IntegrityError)
  4
+from django.db.utils import (DEFAULT_DB_ALIAS,
  5
+    DataError, OperationalError, IntegrityError, InternalError,
  6
+    ProgrammingError, NotSupportedError, DatabaseError,
  7
+    InterfaceError, Error,
  8
+    load_backend, ConnectionHandler, ConnectionRouter)
6 9
 
7 10
 __all__ = ('backend', 'connection', 'connections', 'router', 'DatabaseError',
8 11
     'IntegrityError', 'DEFAULT_DB_ALIAS')
33  django/db/backends/__init__.py
@@ -14,6 +14,7 @@
14 14
 from django.db.backends.signals import connection_created
15 15
 from django.db.backends import util
16 16
 from django.db.transaction import TransactionManagementError
  17
+from django.db.utils import DatabaseErrorWrapper
17 18
 from django.utils.functional import cached_property
18 19
 from django.utils.importlib import import_module
19 20
 from django.utils import six
@@ -57,6 +58,9 @@ def __ne__(self, other):
57 58
     def __hash__(self):
58 59
         return hash(self.alias)
59 60
 
  61
+    def wrap_database_errors(self):
  62
+        return DatabaseErrorWrapper(self.Database)
  63
+
60 64
     def get_connection_params(self):
61 65
         raise NotImplementedError
62 66
 
@@ -70,20 +74,28 @@ def create_cursor(self):
70 74
         raise NotImplementedError
71 75
 
72 76
     def _cursor(self):
73  
-        if self.connection is None:
74  
-            conn_params = self.get_connection_params()
75  
-            self.connection = self.get_new_connection(conn_params)
76  
-            self.init_connection_state()
77  
-            connection_created.send(sender=self.__class__, connection=self)
78  
-        return self.create_cursor()
  77
+        with self.wrap_database_errors():
  78
+            if self.connection is None:
  79
+                conn_params = self.get_connection_params()
  80
+                self.connection = self.get_new_connection(conn_params)
  81
+                self.init_connection_state()
  82
+                connection_created.send(sender=self.__class__, connection=self)
  83
+            return self.create_cursor()
79 84
 
80 85
     def _commit(self):
81 86
         if self.connection is not None:
82  
-            return self.connection.commit()
  87
+            with self.wrap_database_errors():
  88
+                return self.connection.commit()
83 89
 
84 90
     def _rollback(self):
85 91
         if self.connection is not None:
86  
-            return self.connection.rollback()
  92
+            with self.wrap_database_errors():
  93
+                return self.connection.rollback()
  94
+
  95
+    def _close(self):
  96
+        if self.connection is not None:
  97
+            with self.wrap_database_errors():
  98
+                return self.connection.close()
87 99
 
88 100
     def _enter_transaction_management(self, managed):
89 101
         """
@@ -333,8 +345,9 @@ def check_constraints(self, table_names=None):
333 345
 
334 346
     def close(self):
335 347
         self.validate_thread_sharing()
336  
-        if self.connection is not None:
337  
-            self.connection.close()
  348
+        try:
  349
+            self._close()
  350
+        finally:
338 351
             self.connection = None
339 352
         self.set_clean()
340 353
 
14  django/db/backends/mysql/base.py
@@ -116,30 +116,22 @@ def __init__(self, cursor):
116 116
     def execute(self, query, args=None):
117 117
         try:
118 118
             return self.cursor.execute(query, args)
119  
-        except Database.IntegrityError as e:
120  
-            six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
121 119
         except Database.OperationalError as e:
122 120
             # Map some error codes to IntegrityError, since they seem to be
123 121
             # misclassified and Django would prefer the more logical place.
124 122
             if e[0] in self.codes_for_integrityerror:
125 123
                 six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
126  
-            six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
127  
-        except Database.DatabaseError as e:
128  
-            six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
  124
+            raise
129 125
 
130 126
     def executemany(self, query, args):
131 127
         try:
132 128
             return self.cursor.executemany(query, args)
133  
-        except Database.IntegrityError as e:
134  
-            six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
135 129
         except Database.OperationalError as e:
136 130
             # Map some error codes to IntegrityError, since they seem to be
137 131
             # misclassified and Django would prefer the more logical place.
138 132
             if e[0] in self.codes_for_integrityerror:
139 133
                 six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
140  
-            six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
141  
-        except Database.DatabaseError as e:
142  
-            six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
  134
+            raise
143 135
 
144 136
     def __getattr__(self, attr):
145 137
         if attr in self.__dict__:
@@ -391,6 +383,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
391 383
         'iendswith': 'LIKE %s',
392 384
     }
393 385
 
  386
+    Database = Database
  387
+
394 388
     def __init__(self, *args, **kwargs):
395 389
         super(DatabaseWrapper, self).__init__(*args, **kwargs)
396 390
 
16  django/db/backends/oracle/base.py
@@ -501,6 +501,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
501 501
         'iendswith': "LIKEC UPPER(%s) ESCAPE '\\'",
502 502
     })
503 503
 
  504
+    Database = Database
  505
+
504 506
     def __init__(self, *args, **kwargs):
505 507
         super(DatabaseWrapper, self).__init__(*args, **kwargs)
506 508
 
@@ -604,10 +606,6 @@ def _commit(self):
604 606
         if self.connection is not None:
605 607
             try:
606 608
                 return self.connection.commit()
607  
-            except Database.IntegrityError as e:
608  
-                # In case cx_Oracle implements (now or in a future version)
609  
-                # raising this specific exception
610  
-                six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
611 609
             except Database.DatabaseError as e:
612 610
                 # cx_Oracle 5.0.4 raises a cx_Oracle.DatabaseError exception
613 611
                 # with the following attributes and values:
@@ -620,7 +618,7 @@ def _commit(self):
620 618
                 if hasattr(x, 'code') and hasattr(x, 'message') \
621 619
                    and x.code == 2091 and 'ORA-02291' in x.message:
622 620
                     six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
623  
-                six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
  621
+                raise
624 622
 
625 623
     @cached_property
626 624
     def oracle_version(self):
@@ -760,13 +758,11 @@ def execute(self, query, params=None):
760 758
         self._guess_input_sizes([params])
761 759
         try:
762 760
             return self.cursor.execute(query, self._param_generator(params))
763  
-        except Database.IntegrityError as e:
764  
-            six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
765 761
         except Database.DatabaseError as e:
766 762
             # cx_Oracle <= 4.4.0 wrongly raises a DatabaseError for ORA-01400.
767 763
             if hasattr(e.args[0], 'code') and e.args[0].code == 1400 and not isinstance(e, IntegrityError):
768 764
                 six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
769  
-            six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
  765
+            raise
770 766
 
771 767
     def executemany(self, query, params=None):
772 768
         # cx_Oracle doesn't support iterators, convert them to lists
@@ -789,13 +785,11 @@ def executemany(self, query, params=None):
789 785
         try:
790 786
             return self.cursor.executemany(query,
791 787
                                 [self._param_generator(p) for p in formatted])
792  
-        except Database.IntegrityError as e:
793  
-            six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
794 788
         except Database.DatabaseError as e:
795 789
             # cx_Oracle <= 4.4.0 wrongly raises a DatabaseError for ORA-01400.
796 790
             if hasattr(e.args[0], 'code') and e.args[0].code == 1400 and not isinstance(e, IntegrityError):
797 791
                 six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
798  
-            six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
  792
+            raise
799 793
 
800 794
     def fetchone(self):
801 795
         row = self.cursor.fetchone()
45  django/db/backends/postgresql_psycopg2/base.py
@@ -40,40 +40,6 @@ def utc_tzinfo_factory(offset):
40 40
         raise AssertionError("database connection isn't set to UTC")
41 41
     return utc
42 42
 
43  
-class CursorWrapper(object):
44  
-    """
45  
-    A thin wrapper around psycopg2's normal cursor class so that we can catch
46  
-    particular exception instances and reraise them with the right types.
47  
-    """
48  
-
49  
-    def __init__(self, cursor):
50  
-        self.cursor = cursor
51  
-
52  
-    def execute(self, query, args=None):
53  
-        try:
54  
-            return self.cursor.execute(query, args)
55  
-        except Database.IntegrityError as e:
56  
-            six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
57  
-        except Database.DatabaseError as e:
58  
-            six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
59  
-
60  
-    def executemany(self, query, args):
61  
-        try:
62  
-            return self.cursor.executemany(query, args)
63  
-        except Database.IntegrityError as e:
64  
-            six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
65  
-        except Database.DatabaseError as e:
66  
-            six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
67  
-
68  
-    def __getattr__(self, attr):
69  
-        if attr in self.__dict__:
70  
-            return self.__dict__[attr]
71  
-        else:
72  
-            return getattr(self.cursor, attr)
73  
-
74  
-    def __iter__(self):
75  
-        return iter(self.cursor)
76  
-
77 43
 class DatabaseFeatures(BaseDatabaseFeatures):
78 44
     needs_datetime_string_cast = False
79 45
     can_return_id_from_insert = True
@@ -106,6 +72,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
106 72
         'iendswith': 'LIKE UPPER(%s)',
107 73
     }
108 74
 
  75
+    Database = Database
  76
+
109 77
     def __init__(self, *args, **kwargs):
110 78
         super(DatabaseWrapper, self).__init__(*args, **kwargs)
111 79
 
@@ -207,7 +175,7 @@ def init_connection_state(self):
207 175
     def create_cursor(self):
208 176
         cursor = self.connection.cursor()
209 177
         cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None
210  
-        return CursorWrapper(cursor)
  178
+        return cursor
211 179
 
212 180
     def _enter_transaction_management(self, managed):
213 181
         """
@@ -245,10 +213,3 @@ def set_dirty(self):
245 213
         if ((self.transaction_state and self.transaction_state[-1]) or
246 214
                 not self.features.uses_autocommit):
247 215
             super(DatabaseWrapper, self).set_dirty()
248  
-
249  
-    def _commit(self):
250  
-        if self.connection is not None:
251  
-            try:
252  
-                return self.connection.commit()
253  
-            except Database.IntegrityError as e:
254  
-                six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
19  django/db/backends/sqlite3/base.py
@@ -10,7 +10,6 @@
10 10
 import decimal
11 11
 import warnings
12 12
 import re
13  
-import sys
14 13
 
15 14
 from django.db import utils
16 15
 from django.db.backends import *
@@ -291,6 +290,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
291 290
         'iendswith': "LIKE %s ESCAPE '\\'",
292 291
     }
293 292
 
  293
+    Database = Database
  294
+
294 295
     def __init__(self, *args, **kwargs):
295 296
         super(DatabaseWrapper, self).__init__(*args, **kwargs)
296 297
 
@@ -398,24 +399,14 @@ class SQLiteCursorWrapper(Database.Cursor):
398 399
     """
399 400
     def execute(self, query, params=()):
400 401
         query = self.convert_query(query)
401  
-        try:
402  
-            return Database.Cursor.execute(self, query, params)
403  
-        except Database.IntegrityError as e:
404  
-            six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
405  
-        except Database.DatabaseError as e:
406  
-            six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
  402
+        return Database.Cursor.execute(self, query, params)
407 403
 
408 404
     def executemany(self, query, param_list):
409 405
         query = self.convert_query(query)
410  
-        try:
411  
-            return Database.Cursor.executemany(self, query, param_list)
412  
-        except Database.IntegrityError as e:
413  
-            six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
414  
-        except Database.DatabaseError as e:
415  
-            six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
  406
+        return Database.Cursor.executemany(self, query, param_list)
416 407
 
417 408
     def convert_query(self, query):
418  
-        return FORMAT_QMARK_REGEX.sub('?', query).replace('%%','%')
  409
+        return FORMAT_QMARK_REGEX.sub('?', query).replace('%%', '%')
419 410
 
420 411
 def _sqlite_date_extract(lookup_type, dt):
421 412
     if dt is None:
13  django/db/backends/util.py
@@ -22,7 +22,12 @@ def __init__(self, cursor, db):
22 22
     def __getattr__(self, attr):
23 23
         if attr in ('execute', 'executemany', 'callproc'):
24 24
             self.db.set_dirty()
25  
-        return getattr(self.cursor, attr)
  25
+        cursor_attr = getattr(self.cursor, attr)
  26
+        if attr in ('callproc', 'close', 'execute', 'executemany',
  27
+                    'fetchone', 'fetchmany', 'fetchall', 'nextset'):
  28
+            return self.db.wrap_database_errors()(cursor_attr)
  29
+        else:
  30
+            return cursor_attr
26 31
 
27 32
     def __iter__(self):
28 33
         return iter(self.cursor)
@@ -34,7 +39,8 @@ def execute(self, sql, params=()):
34 39
         self.db.set_dirty()
35 40
         start = time()
36 41
         try:
37  
-            return self.cursor.execute(sql, params)
  42
+            with self.db.wrap_database_errors():
  43
+                return self.cursor.execute(sql, params)
38 44
         finally:
39 45
             stop = time()
40 46
             duration = stop - start
@@ -51,7 +57,8 @@ def executemany(self, sql, param_list):
51 57
         self.db.set_dirty()
52 58
         start = time()
53 59
         try:
54  
-            return self.cursor.executemany(sql, param_list)
  60
+            with self.db.wrap_database_errors():
  61
+                return self.cursor.executemany(sql, param_list)
55 62
         finally:
56 63
             stop = time()
57 64
             duration = stop - start
80  django/db/utils.py
... ...
@@ -1,3 +1,4 @@
  1
+from functools import wraps
1 2
 import os
2 3
 import pkgutil
3 4
 from threading import local
@@ -12,16 +13,87 @@
12 13
 
13 14
 DEFAULT_DB_ALIAS = 'default'
14 15
 
15  
-# Define some exceptions that mirror the PEP249 interface.
16  
-# We will rethrow any backend-specific errors using these
17  
-# common wrappers
18  
-class DatabaseError(Exception):
  16
+
  17
+class Error(StandardError):
  18
+    pass
  19
+
  20
+
  21
+class InterfaceError(Error):
  22
+    pass
  23
+
  24
+
  25
+class DatabaseError(Error):
19 26
     pass
20 27
 
  28
+
  29
+class DataError(DatabaseError):
  30
+    pass
  31
+
  32
+
  33
+class OperationalError(DatabaseError):
  34
+    pass
  35
+
  36
+
21 37
 class IntegrityError(DatabaseError):
22 38
     pass
23 39
 
24 40
 
  41
+class InternalError(DatabaseError):
  42
+    pass
  43
+
  44
+
  45
+class ProgrammingError(DatabaseError):
  46
+    pass
  47
+
  48
+
  49
+class NotSupportedError(DatabaseError):
  50
+    pass
  51
+
  52
+
  53
+class DatabaseErrorWrapper(object):
  54
+    """
  55
+    Context manager and decorator that re-throws backend-specific database
  56
+    exceptions using Django's common wrappers.
  57
+    """
  58
+
  59
+    def __init__(self, database):
  60
+        """
  61
+        database is a module defining PEP-249 exceptions.
  62
+        """
  63
+        self.database = database
  64
+
  65
+    def __enter__(self):
  66
+        pass
  67
+
  68
+    def __exit__(self, exc_type, exc_value, traceback):
  69
+        if exc_type is None:
  70
+            return
  71
+        for dj_exc_type in (
  72
+                DataError,
  73
+                OperationalError,
  74
+                IntegrityError,
  75
+                InternalError,
  76
+                ProgrammingError,
  77
+                NotSupportedError,
  78
+                DatabaseError,
  79
+                InterfaceError,
  80
+                Error,
  81
+            ):
  82
+            db_exc_type = getattr(self.database, dj_exc_type.__name__)
  83
+            if issubclass(exc_type, db_exc_type):
  84
+                dj_exc_value = dj_exc_type(*tuple(exc_value.args))
  85
+                if six.PY3:
  86
+                    dj_exc_value.__cause__ = exc_value
  87
+                six.reraise(dj_exc_type, dj_exc_value, traceback)
  88
+
  89
+    def __call__(self, func):
  90
+        @wraps(func)
  91
+        def inner(*args, **kwargs):
  92
+            with self:
  93
+                return func(*args, **kwargs)
  94
+        return inner
  95
+
  96
+
25 97
 def load_backend(backend_name):
26 98
     # Look for a fully qualified database backend name
27 99
     try:
18  docs/ref/exceptions.txt
@@ -119,18 +119,28 @@ NoReverseMatch
119 119
 Database Exceptions
120 120
 ===================
121 121
 
122  
-Django wraps the standard database exceptions :exc:`DatabaseError` and
123  
-:exc:`IntegrityError` so that your Django code has a guaranteed common
124  
-implementation of these classes. These database exceptions are
125  
-provided in :mod:`django.db`.
  122
+Django wraps the standard database exceptions so that your Django code has a
  123
+guaranteed common implementation of these classes. These database exceptions
  124
+are provided in :mod:`django.db`.
126 125
 
  126
+.. exception:: Error
  127
+.. exception:: InterfaceError
127 128
 .. exception:: DatabaseError
  129
+.. exception:: DataError
  130
+.. exception:: OperationalError
128 131
 .. exception:: IntegrityError
  132
+.. exception:: InternalError
  133
+.. exception:: ProgrammingError
  134
+.. exception:: NotSupportedError
129 135
 
130 136
 The Django wrappers for database exceptions behave exactly the same as
131 137
 the underlying database exceptions. See :pep:`249`, the Python Database API
132 138
 Specification v2.0, for further information.
133 139
 
  140
+.. versionchanged:: 1.6
  141
+    Previous version of Django only wrapped ``DatabaseError`` and
  142
+    ``IntegrityError``.
  143
+
134 144
 .. exception:: models.ProtectedError
135 145
 
136 146
 Raised to prevent deletion of referenced objects when using
2  docs/releases/1.6.txt
@@ -60,6 +60,8 @@ Minor features
60 60
 * In addition to :lookup:`year`, :lookup:`month` and :lookup:`day`, the ORM
61 61
   now supports :lookup:`hour`, :lookup:`minute` and :lookup:`second` lookups.
62 62
 
  63
+* Django now wraps all PEP-249 exceptions.
  64
+
63 65
 * The default widgets for :class:`~django.forms.EmailField`,
64 66
   :class:`~django.forms.URLField`, :class:`~django.forms.IntegerField`,
65 67
   :class:`~django.forms.FloatField` and :class:`~django.forms.DecimalField` use

0 notes on commit 59a3520

Please sign in to comment.
Something went wrong with that request. Please try again.