summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAymeric Augustin <aymeric.augustin@m4x.org>2013-02-26 14:53:34 +0100
committerAymeric Augustin <aymeric.augustin@m4x.org>2013-02-27 17:26:54 +0100
commit59a352087591a26023412cbcb830cd1d34fc9b99 (patch)
tree7f230aa9c8d3e02847a0f0f24c74c777187a734a
parent50328f0a618674b7143d86acaa7016c5293e9774 (diff)
Refactored database exceptions wrapping.
Squashed commit of the following: commit 2181d833ed1a2e422494738dcef311164c4e097e Author: Aymeric Augustin <aymeric.augustin@m4x.org> Date: Wed Feb 27 14:28:39 2013 +0100 Fixed #15901 -- Wrapped all PEP-249 exceptions. commit 5476a5d93c19aa2f928c497d39ce6e33f52694e2 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 9365fad0a650328002fb424457d675a273c95802 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 1b463b765f2826f73a8d9266795cd5da4f8d5e9e 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 524bc7345a724bf526bdd2dd1bcf5ede67d6bb5c 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 a66746bb5f0839f35543222787fce3b6a0d0a3ea 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.
-rw-r--r--django/db/__init__.py7
-rw-r--r--django/db/backends/__init__.py33
-rw-r--r--django/db/backends/mysql/base.py14
-rw-r--r--django/db/backends/oracle/base.py16
-rw-r--r--django/db/backends/postgresql_psycopg2/base.py45
-rw-r--r--django/db/backends/sqlite3/base.py19
-rw-r--r--django/db/backends/util.py13
-rw-r--r--django/db/utils.py80
-rw-r--r--docs/ref/exceptions.txt18
-rw-r--r--docs/releases/1.6.txt2
10 files changed, 147 insertions, 100 deletions
diff --git a/django/db/__init__.py b/django/db/__init__.py
index 32e42bbfe9..e76c6c3268 100644
--- a/django/db/__init__.py
+++ b/django/db/__init__.py
@@ -1,8 +1,11 @@
from django.conf import settings
from django.core import signals
from django.core.exceptions import ImproperlyConfigured
-from django.db.utils import (ConnectionHandler, ConnectionRouter,
- load_backend, DEFAULT_DB_ALIAS, DatabaseError, IntegrityError)
+from django.db.utils import (DEFAULT_DB_ALIAS,
+ DataError, OperationalError, IntegrityError, InternalError,
+ ProgrammingError, NotSupportedError, DatabaseError,
+ InterfaceError, Error,
+ load_backend, ConnectionHandler, ConnectionRouter)
__all__ = ('backend', 'connection', 'connections', 'router', 'DatabaseError',
'IntegrityError', 'DEFAULT_DB_ALIAS')
diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py
index 46db1910f9..49e07cfa9e 100644
--- a/django/db/backends/__init__.py
+++ b/django/db/backends/__init__.py
@@ -14,6 +14,7 @@ from django.db import DEFAULT_DB_ALIAS
from django.db.backends.signals import connection_created
from django.db.backends import util
from django.db.transaction import TransactionManagementError
+from django.db.utils import DatabaseErrorWrapper
from django.utils.functional import cached_property
from django.utils.importlib import import_module
from django.utils import six
@@ -57,6 +58,9 @@ class BaseDatabaseWrapper(object):
def __hash__(self):
return hash(self.alias)
+ def wrap_database_errors(self):
+ return DatabaseErrorWrapper(self.Database)
+
def get_connection_params(self):
raise NotImplementedError
@@ -70,20 +74,28 @@ class BaseDatabaseWrapper(object):
raise NotImplementedError
def _cursor(self):
- if self.connection is None:
- conn_params = self.get_connection_params()
- self.connection = self.get_new_connection(conn_params)
- self.init_connection_state()
- connection_created.send(sender=self.__class__, connection=self)
- return self.create_cursor()
+ with self.wrap_database_errors():
+ if self.connection is None:
+ conn_params = self.get_connection_params()
+ self.connection = self.get_new_connection(conn_params)
+ self.init_connection_state()
+ connection_created.send(sender=self.__class__, connection=self)
+ return self.create_cursor()
def _commit(self):
if self.connection is not None:
- return self.connection.commit()
+ with self.wrap_database_errors():
+ return self.connection.commit()
def _rollback(self):
if self.connection is not None:
- return self.connection.rollback()
+ with self.wrap_database_errors():
+ return self.connection.rollback()
+
+ def _close(self):
+ if self.connection is not None:
+ with self.wrap_database_errors():
+ return self.connection.close()
def _enter_transaction_management(self, managed):
"""
@@ -333,8 +345,9 @@ class BaseDatabaseWrapper(object):
def close(self):
self.validate_thread_sharing()
- if self.connection is not None:
- self.connection.close()
+ try:
+ self._close()
+ finally:
self.connection = None
self.set_clean()
diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py
index fc2ff31581..4bfd3c4481 100644
--- a/django/db/backends/mysql/base.py
+++ b/django/db/backends/mysql/base.py
@@ -116,30 +116,22 @@ class CursorWrapper(object):
def execute(self, query, args=None):
try:
return self.cursor.execute(query, args)
- except Database.IntegrityError as e:
- six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
except Database.OperationalError as e:
# Map some error codes to IntegrityError, since they seem to be
# misclassified and Django would prefer the more logical place.
if e[0] in self.codes_for_integrityerror:
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
- six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
- except Database.DatabaseError as e:
- six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
+ raise
def executemany(self, query, args):
try:
return self.cursor.executemany(query, args)
- except Database.IntegrityError as e:
- six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
except Database.OperationalError as e:
# Map some error codes to IntegrityError, since they seem to be
# misclassified and Django would prefer the more logical place.
if e[0] in self.codes_for_integrityerror:
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
- six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
- except Database.DatabaseError as e:
- six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
+ raise
def __getattr__(self, attr):
if attr in self.__dict__:
@@ -391,6 +383,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
'iendswith': 'LIKE %s',
}
+ Database = Database
+
def __init__(self, *args, **kwargs):
super(DatabaseWrapper, self).__init__(*args, **kwargs)
diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py
index 3c799f01c1..d35e814d1f 100644
--- a/django/db/backends/oracle/base.py
+++ b/django/db/backends/oracle/base.py
@@ -501,6 +501,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
'iendswith': "LIKEC UPPER(%s) ESCAPE '\\'",
})
+ Database = Database
+
def __init__(self, *args, **kwargs):
super(DatabaseWrapper, self).__init__(*args, **kwargs)
@@ -604,10 +606,6 @@ class DatabaseWrapper(BaseDatabaseWrapper):
if self.connection is not None:
try:
return self.connection.commit()
- except Database.IntegrityError as e:
- # In case cx_Oracle implements (now or in a future version)
- # raising this specific exception
- six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
except Database.DatabaseError as e:
# cx_Oracle 5.0.4 raises a cx_Oracle.DatabaseError exception
# with the following attributes and values:
@@ -620,7 +618,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
if hasattr(x, 'code') and hasattr(x, 'message') \
and x.code == 2091 and 'ORA-02291' in x.message:
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
- six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
+ raise
@cached_property
def oracle_version(self):
@@ -760,13 +758,11 @@ class FormatStylePlaceholderCursor(object):
self._guess_input_sizes([params])
try:
return self.cursor.execute(query, self._param_generator(params))
- except Database.IntegrityError as e:
- six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
except Database.DatabaseError as e:
# cx_Oracle <= 4.4.0 wrongly raises a DatabaseError for ORA-01400.
if hasattr(e.args[0], 'code') and e.args[0].code == 1400 and not isinstance(e, IntegrityError):
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
- six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
+ raise
def executemany(self, query, params=None):
# cx_Oracle doesn't support iterators, convert them to lists
@@ -789,13 +785,11 @@ class FormatStylePlaceholderCursor(object):
try:
return self.cursor.executemany(query,
[self._param_generator(p) for p in formatted])
- except Database.IntegrityError as e:
- six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
except Database.DatabaseError as e:
# cx_Oracle <= 4.4.0 wrongly raises a DatabaseError for ORA-01400.
if hasattr(e.args[0], 'code') and e.args[0].code == 1400 and not isinstance(e, IntegrityError):
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
- six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
+ raise
def fetchone(self):
row = self.cursor.fetchone()
diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py
index bf129c0758..fb04072494 100644
--- a/django/db/backends/postgresql_psycopg2/base.py
+++ b/django/db/backends/postgresql_psycopg2/base.py
@@ -40,40 +40,6 @@ def utc_tzinfo_factory(offset):
raise AssertionError("database connection isn't set to UTC")
return utc
-class CursorWrapper(object):
- """
- A thin wrapper around psycopg2's normal cursor class so that we can catch
- particular exception instances and reraise them with the right types.
- """
-
- def __init__(self, cursor):
- self.cursor = cursor
-
- def execute(self, query, args=None):
- try:
- return self.cursor.execute(query, args)
- except Database.IntegrityError as e:
- six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
- except Database.DatabaseError as e:
- six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
-
- def executemany(self, query, args):
- try:
- return self.cursor.executemany(query, args)
- except Database.IntegrityError as e:
- six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
- except Database.DatabaseError as e:
- six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
-
- def __getattr__(self, attr):
- if attr in self.__dict__:
- return self.__dict__[attr]
- else:
- return getattr(self.cursor, attr)
-
- def __iter__(self):
- return iter(self.cursor)
-
class DatabaseFeatures(BaseDatabaseFeatures):
needs_datetime_string_cast = False
can_return_id_from_insert = True
@@ -106,6 +72,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
'iendswith': 'LIKE UPPER(%s)',
}
+ Database = Database
+
def __init__(self, *args, **kwargs):
super(DatabaseWrapper, self).__init__(*args, **kwargs)
@@ -207,7 +175,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
def create_cursor(self):
cursor = self.connection.cursor()
cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None
- return CursorWrapper(cursor)
+ return cursor
def _enter_transaction_management(self, managed):
"""
@@ -245,10 +213,3 @@ class DatabaseWrapper(BaseDatabaseWrapper):
if ((self.transaction_state and self.transaction_state[-1]) or
not self.features.uses_autocommit):
super(DatabaseWrapper, self).set_dirty()
-
- def _commit(self):
- if self.connection is not None:
- try:
- return self.connection.commit()
- except Database.IntegrityError as e:
- six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py
index 7ddaaf8fe3..6bf1ffc469 100644
--- a/django/db/backends/sqlite3/base.py
+++ b/django/db/backends/sqlite3/base.py
@@ -10,7 +10,6 @@ import datetime
import decimal
import warnings
import re
-import sys
from django.db import utils
from django.db.backends import *
@@ -291,6 +290,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
'iendswith': "LIKE %s ESCAPE '\\'",
}
+ Database = Database
+
def __init__(self, *args, **kwargs):
super(DatabaseWrapper, self).__init__(*args, **kwargs)
@@ -398,24 +399,14 @@ class SQLiteCursorWrapper(Database.Cursor):
"""
def execute(self, query, params=()):
query = self.convert_query(query)
- try:
- return Database.Cursor.execute(self, query, params)
- except Database.IntegrityError as e:
- six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
- except Database.DatabaseError as e:
- six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
+ return Database.Cursor.execute(self, query, params)
def executemany(self, query, param_list):
query = self.convert_query(query)
- try:
- return Database.Cursor.executemany(self, query, param_list)
- except Database.IntegrityError as e:
- six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
- except Database.DatabaseError as e:
- six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
+ return Database.Cursor.executemany(self, query, param_list)
def convert_query(self, query):
- return FORMAT_QMARK_REGEX.sub('?', query).replace('%%','%')
+ return FORMAT_QMARK_REGEX.sub('?', query).replace('%%', '%')
def _sqlite_date_extract(lookup_type, dt):
if dt is None:
diff --git a/django/db/backends/util.py b/django/db/backends/util.py
index ebab982a04..5eb6626fc7 100644
--- a/django/db/backends/util.py
+++ b/django/db/backends/util.py
@@ -22,7 +22,12 @@ class CursorWrapper(object):
def __getattr__(self, attr):
if attr in ('execute', 'executemany', 'callproc'):
self.db.set_dirty()
- return getattr(self.cursor, attr)
+ cursor_attr = getattr(self.cursor, attr)
+ if attr in ('callproc', 'close', 'execute', 'executemany',
+ 'fetchone', 'fetchmany', 'fetchall', 'nextset'):
+ return self.db.wrap_database_errors()(cursor_attr)
+ else:
+ return cursor_attr
def __iter__(self):
return iter(self.cursor)
@@ -34,7 +39,8 @@ class CursorDebugWrapper(CursorWrapper):
self.db.set_dirty()
start = time()
try:
- return self.cursor.execute(sql, params)
+ with self.db.wrap_database_errors():
+ return self.cursor.execute(sql, params)
finally:
stop = time()
duration = stop - start
@@ -51,7 +57,8 @@ class CursorDebugWrapper(CursorWrapper):
self.db.set_dirty()
start = time()
try:
- return self.cursor.executemany(sql, param_list)
+ with self.db.wrap_database_errors():
+ return self.cursor.executemany(sql, param_list)
finally:
stop = time()
duration = stop - start
diff --git a/django/db/utils.py b/django/db/utils.py
index 91fa774ed4..c841e06f3e 100644
--- a/django/db/utils.py
+++ b/django/db/utils.py
@@ -1,3 +1,4 @@
+from functools import wraps
import os
import pkgutil
from threading import local
@@ -12,16 +13,87 @@ from django.utils import six
DEFAULT_DB_ALIAS = 'default'
-# Define some exceptions that mirror the PEP249 interface.
-# We will rethrow any backend-specific errors using these
-# common wrappers
-class DatabaseError(Exception):
+
+class Error(StandardError):
+ pass
+
+
+class InterfaceError(Error):
+ pass
+
+
+class DatabaseError(Error):
+ pass
+
+
+class DataError(DatabaseError):
+ pass
+
+
+class OperationalError(DatabaseError):
pass
+
class IntegrityError(DatabaseError):
pass
+class InternalError(DatabaseError):
+ pass
+
+
+class ProgrammingError(DatabaseError):
+ pass
+
+
+class NotSupportedError(DatabaseError):
+ pass
+
+
+class DatabaseErrorWrapper(object):
+ """
+ Context manager and decorator that re-throws backend-specific database
+ exceptions using Django's common wrappers.
+ """
+
+ def __init__(self, database):
+ """
+ database is a module defining PEP-249 exceptions.
+ """
+ self.database = database
+
+ def __enter__(self):
+ pass
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ if exc_type is None:
+ return
+ for dj_exc_type in (
+ DataError,
+ OperationalError,
+ IntegrityError,
+ InternalError,
+ ProgrammingError,
+ NotSupportedError,
+ DatabaseError,
+ InterfaceError,
+ Error,
+ ):
+ db_exc_type = getattr(self.database, dj_exc_type.__name__)
+ if issubclass(exc_type, db_exc_type):
+ dj_exc_value = dj_exc_type(*tuple(exc_value.args))
+ if six.PY3:
+ dj_exc_value.__cause__ = exc_value
+ six.reraise(dj_exc_type, dj_exc_value, traceback)
+
+ def __call__(self, func):
+ @wraps(func)
+ def inner(*args, **kwargs):
+ with self:
+ return func(*args, **kwargs)
+ return inner
+
+
def load_backend(backend_name):
# Look for a fully qualified database backend name
try:
diff --git a/docs/ref/exceptions.txt b/docs/ref/exceptions.txt
index f123ae2e59..93bb9ed251 100644
--- a/docs/ref/exceptions.txt
+++ b/docs/ref/exceptions.txt
@@ -119,18 +119,28 @@ NoReverseMatch
Database Exceptions
===================
-Django wraps the standard database exceptions :exc:`DatabaseError` and
-:exc:`IntegrityError` so that your Django code has a guaranteed common
-implementation of these classes. These database exceptions are
-provided in :mod:`django.db`.
+Django wraps the standard database exceptions so that your Django code has a
+guaranteed common implementation of these classes. These database exceptions
+are provided in :mod:`django.db`.
+.. exception:: Error
+.. exception:: InterfaceError
.. exception:: DatabaseError
+.. exception:: DataError
+.. exception:: OperationalError
.. exception:: IntegrityError
+.. exception:: InternalError
+.. exception:: ProgrammingError
+.. exception:: NotSupportedError
The Django wrappers for database exceptions behave exactly the same as
the underlying database exceptions. See :pep:`249`, the Python Database API
Specification v2.0, for further information.
+.. versionchanged:: 1.6
+ Previous version of Django only wrapped ``DatabaseError`` and
+ ``IntegrityError``.
+
.. exception:: models.ProtectedError
Raised to prevent deletion of referenced objects when using
diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt
index 1083d4d515..c6a4fb2d5d 100644
--- a/docs/releases/1.6.txt
+++ b/docs/releases/1.6.txt
@@ -60,6 +60,8 @@ Minor features
* In addition to :lookup:`year`, :lookup:`month` and :lookup:`day`, the ORM
now supports :lookup:`hour`, :lookup:`minute` and :lookup:`second` lookups.
+* Django now wraps all PEP-249 exceptions.
+
* The default widgets for :class:`~django.forms.EmailField`,
:class:`~django.forms.URLField`, :class:`~django.forms.IntegerField`,
:class:`~django.forms.FloatField` and :class:`~django.forms.DecimalField` use