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.
This commit is contained in:
Aymeric Augustin 2013-02-26 14:53:34 +01:00
parent 50328f0a61
commit 59a3520875
10 changed files with 147 additions and 100 deletions

View File

@ -1,8 +1,11 @@
from django.conf import settings from django.conf import settings
from django.core import signals from django.core import signals
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db.utils import (ConnectionHandler, ConnectionRouter, from django.db.utils import (DEFAULT_DB_ALIAS,
load_backend, DEFAULT_DB_ALIAS, DatabaseError, IntegrityError) DataError, OperationalError, IntegrityError, InternalError,
ProgrammingError, NotSupportedError, DatabaseError,
InterfaceError, Error,
load_backend, ConnectionHandler, ConnectionRouter)
__all__ = ('backend', 'connection', 'connections', 'router', 'DatabaseError', __all__ = ('backend', 'connection', 'connections', 'router', 'DatabaseError',
'IntegrityError', 'DEFAULT_DB_ALIAS') 'IntegrityError', 'DEFAULT_DB_ALIAS')

View File

@ -14,6 +14,7 @@ from django.db import DEFAULT_DB_ALIAS
from django.db.backends.signals import connection_created from django.db.backends.signals import connection_created
from django.db.backends import util from django.db.backends import util
from django.db.transaction import TransactionManagementError from django.db.transaction import TransactionManagementError
from django.db.utils import DatabaseErrorWrapper
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.importlib import import_module from django.utils.importlib import import_module
from django.utils import six from django.utils import six
@ -57,6 +58,9 @@ class BaseDatabaseWrapper(object):
def __hash__(self): def __hash__(self):
return hash(self.alias) return hash(self.alias)
def wrap_database_errors(self):
return DatabaseErrorWrapper(self.Database)
def get_connection_params(self): def get_connection_params(self):
raise NotImplementedError raise NotImplementedError
@ -70,20 +74,28 @@ class BaseDatabaseWrapper(object):
raise NotImplementedError raise NotImplementedError
def _cursor(self): def _cursor(self):
if self.connection is None: with self.wrap_database_errors():
conn_params = self.get_connection_params() if self.connection is None:
self.connection = self.get_new_connection(conn_params) conn_params = self.get_connection_params()
self.init_connection_state() self.connection = self.get_new_connection(conn_params)
connection_created.send(sender=self.__class__, connection=self) self.init_connection_state()
return self.create_cursor() connection_created.send(sender=self.__class__, connection=self)
return self.create_cursor()
def _commit(self): def _commit(self):
if self.connection is not None: if self.connection is not None:
return self.connection.commit() with self.wrap_database_errors():
return self.connection.commit()
def _rollback(self): def _rollback(self):
if self.connection is not None: 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): def _enter_transaction_management(self, managed):
""" """
@ -333,8 +345,9 @@ class BaseDatabaseWrapper(object):
def close(self): def close(self):
self.validate_thread_sharing() self.validate_thread_sharing()
if self.connection is not None: try:
self.connection.close() self._close()
finally:
self.connection = None self.connection = None
self.set_clean() self.set_clean()

View File

@ -116,30 +116,22 @@ class CursorWrapper(object):
def execute(self, query, args=None): def execute(self, query, args=None):
try: try:
return self.cursor.execute(query, args) 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: except Database.OperationalError as e:
# Map some error codes to IntegrityError, since they seem to be # Map some error codes to IntegrityError, since they seem to be
# misclassified and Django would prefer the more logical place. # misclassified and Django would prefer the more logical place.
if e[0] in self.codes_for_integrityerror: if e[0] in self.codes_for_integrityerror:
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2]) 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
except Database.DatabaseError as e:
six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
def executemany(self, query, args): def executemany(self, query, args):
try: try:
return self.cursor.executemany(query, args) 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: except Database.OperationalError as e:
# Map some error codes to IntegrityError, since they seem to be # Map some error codes to IntegrityError, since they seem to be
# misclassified and Django would prefer the more logical place. # misclassified and Django would prefer the more logical place.
if e[0] in self.codes_for_integrityerror: if e[0] in self.codes_for_integrityerror:
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2]) 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
except Database.DatabaseError as e:
six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
def __getattr__(self, attr): def __getattr__(self, attr):
if attr in self.__dict__: if attr in self.__dict__:
@ -391,6 +383,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
'iendswith': 'LIKE %s', 'iendswith': 'LIKE %s',
} }
Database = Database
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(DatabaseWrapper, self).__init__(*args, **kwargs) super(DatabaseWrapper, self).__init__(*args, **kwargs)

View File

@ -501,6 +501,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
'iendswith': "LIKEC UPPER(%s) ESCAPE '\\'", 'iendswith': "LIKEC UPPER(%s) ESCAPE '\\'",
}) })
Database = Database
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(DatabaseWrapper, self).__init__(*args, **kwargs) super(DatabaseWrapper, self).__init__(*args, **kwargs)
@ -604,10 +606,6 @@ class DatabaseWrapper(BaseDatabaseWrapper):
if self.connection is not None: if self.connection is not None:
try: try:
return self.connection.commit() 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: except Database.DatabaseError as e:
# cx_Oracle 5.0.4 raises a cx_Oracle.DatabaseError exception # cx_Oracle 5.0.4 raises a cx_Oracle.DatabaseError exception
# with the following attributes and values: # with the following attributes and values:
@ -620,7 +618,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
if hasattr(x, 'code') and hasattr(x, 'message') \ if hasattr(x, 'code') and hasattr(x, 'message') \
and x.code == 2091 and 'ORA-02291' in 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.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 @cached_property
def oracle_version(self): def oracle_version(self):
@ -760,13 +758,11 @@ class FormatStylePlaceholderCursor(object):
self._guess_input_sizes([params]) self._guess_input_sizes([params])
try: try:
return self.cursor.execute(query, self._param_generator(params)) 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: except Database.DatabaseError as e:
# cx_Oracle <= 4.4.0 wrongly raises a DatabaseError for ORA-01400. # 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): 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.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): def executemany(self, query, params=None):
# cx_Oracle doesn't support iterators, convert them to lists # cx_Oracle doesn't support iterators, convert them to lists
@ -789,13 +785,11 @@ class FormatStylePlaceholderCursor(object):
try: try:
return self.cursor.executemany(query, return self.cursor.executemany(query,
[self._param_generator(p) for p in formatted]) [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: except Database.DatabaseError as e:
# cx_Oracle <= 4.4.0 wrongly raises a DatabaseError for ORA-01400. # 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): 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.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): def fetchone(self):
row = self.cursor.fetchone() row = self.cursor.fetchone()

View File

@ -40,40 +40,6 @@ def utc_tzinfo_factory(offset):
raise AssertionError("database connection isn't set to UTC") raise AssertionError("database connection isn't set to UTC")
return 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): class DatabaseFeatures(BaseDatabaseFeatures):
needs_datetime_string_cast = False needs_datetime_string_cast = False
can_return_id_from_insert = True can_return_id_from_insert = True
@ -106,6 +72,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
'iendswith': 'LIKE UPPER(%s)', 'iendswith': 'LIKE UPPER(%s)',
} }
Database = Database
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(DatabaseWrapper, self).__init__(*args, **kwargs) super(DatabaseWrapper, self).__init__(*args, **kwargs)
@ -207,7 +175,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
def create_cursor(self): def create_cursor(self):
cursor = self.connection.cursor() cursor = self.connection.cursor()
cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None
return CursorWrapper(cursor) return cursor
def _enter_transaction_management(self, managed): def _enter_transaction_management(self, managed):
""" """
@ -245,10 +213,3 @@ class DatabaseWrapper(BaseDatabaseWrapper):
if ((self.transaction_state and self.transaction_state[-1]) or if ((self.transaction_state and self.transaction_state[-1]) or
not self.features.uses_autocommit): not self.features.uses_autocommit):
super(DatabaseWrapper, self).set_dirty() 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])

View File

@ -10,7 +10,6 @@ import datetime
import decimal import decimal
import warnings import warnings
import re import re
import sys
from django.db import utils from django.db import utils
from django.db.backends import * from django.db.backends import *
@ -291,6 +290,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
'iendswith': "LIKE %s ESCAPE '\\'", 'iendswith': "LIKE %s ESCAPE '\\'",
} }
Database = Database
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(DatabaseWrapper, self).__init__(*args, **kwargs) super(DatabaseWrapper, self).__init__(*args, **kwargs)
@ -398,24 +399,14 @@ class SQLiteCursorWrapper(Database.Cursor):
""" """
def execute(self, query, params=()): def execute(self, query, params=()):
query = self.convert_query(query) query = self.convert_query(query)
try: return Database.Cursor.execute(self, query, params)
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])
def executemany(self, query, param_list): def executemany(self, query, param_list):
query = self.convert_query(query) query = self.convert_query(query)
try: return Database.Cursor.executemany(self, query, param_list)
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])
def convert_query(self, query): 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): def _sqlite_date_extract(lookup_type, dt):
if dt is None: if dt is None:

View File

@ -22,7 +22,12 @@ class CursorWrapper(object):
def __getattr__(self, attr): def __getattr__(self, attr):
if attr in ('execute', 'executemany', 'callproc'): if attr in ('execute', 'executemany', 'callproc'):
self.db.set_dirty() 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): def __iter__(self):
return iter(self.cursor) return iter(self.cursor)
@ -34,7 +39,8 @@ class CursorDebugWrapper(CursorWrapper):
self.db.set_dirty() self.db.set_dirty()
start = time() start = time()
try: try:
return self.cursor.execute(sql, params) with self.db.wrap_database_errors():
return self.cursor.execute(sql, params)
finally: finally:
stop = time() stop = time()
duration = stop - start duration = stop - start
@ -51,7 +57,8 @@ class CursorDebugWrapper(CursorWrapper):
self.db.set_dirty() self.db.set_dirty()
start = time() start = time()
try: try:
return self.cursor.executemany(sql, param_list) with self.db.wrap_database_errors():
return self.cursor.executemany(sql, param_list)
finally: finally:
stop = time() stop = time()
duration = stop - start duration = stop - start

View File

@ -1,3 +1,4 @@
from functools import wraps
import os import os
import pkgutil import pkgutil
from threading import local from threading import local
@ -12,16 +13,87 @@ from django.utils import six
DEFAULT_DB_ALIAS = 'default' DEFAULT_DB_ALIAS = 'default'
# Define some exceptions that mirror the PEP249 interface.
# We will rethrow any backend-specific errors using these class Error(StandardError):
# common wrappers
class DatabaseError(Exception):
pass pass
class InterfaceError(Error):
pass
class DatabaseError(Error):
pass
class DataError(DatabaseError):
pass
class OperationalError(DatabaseError):
pass
class IntegrityError(DatabaseError): class IntegrityError(DatabaseError):
pass 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): def load_backend(backend_name):
# Look for a fully qualified database backend name # Look for a fully qualified database backend name
try: try:

View File

@ -119,18 +119,28 @@ NoReverseMatch
Database Exceptions Database Exceptions
=================== ===================
Django wraps the standard database exceptions :exc:`DatabaseError` and Django wraps the standard database exceptions so that your Django code has a
:exc:`IntegrityError` so that your Django code has a guaranteed common guaranteed common implementation of these classes. These database exceptions
implementation of these classes. These database exceptions are are provided in :mod:`django.db`.
provided in :mod:`django.db`.
.. exception:: Error
.. exception:: InterfaceError
.. exception:: DatabaseError .. exception:: DatabaseError
.. exception:: DataError
.. exception:: OperationalError
.. exception:: IntegrityError .. exception:: IntegrityError
.. exception:: InternalError
.. exception:: ProgrammingError
.. exception:: NotSupportedError
The Django wrappers for database exceptions behave exactly the same as The Django wrappers for database exceptions behave exactly the same as
the underlying database exceptions. See :pep:`249`, the Python Database API the underlying database exceptions. See :pep:`249`, the Python Database API
Specification v2.0, for further information. Specification v2.0, for further information.
.. versionchanged:: 1.6
Previous version of Django only wrapped ``DatabaseError`` and
``IntegrityError``.
.. exception:: models.ProtectedError .. exception:: models.ProtectedError
Raised to prevent deletion of referenced objects when using Raised to prevent deletion of referenced objects when using

View File

@ -60,6 +60,8 @@ Minor features
* In addition to :lookup:`year`, :lookup:`month` and :lookup:`day`, the ORM * In addition to :lookup:`year`, :lookup:`month` and :lookup:`day`, the ORM
now supports :lookup:`hour`, :lookup:`minute` and :lookup:`second` lookups. 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`, * The default widgets for :class:`~django.forms.EmailField`,
:class:`~django.forms.URLField`, :class:`~django.forms.IntegerField`, :class:`~django.forms.URLField`, :class:`~django.forms.IntegerField`,
:class:`~django.forms.FloatField` and :class:`~django.forms.DecimalField` use :class:`~django.forms.FloatField` and :class:`~django.forms.DecimalField` use