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