Ported MySQLdb to the new database exception hierarchy.

This commit is contained in:
Tom Keffer
2017-02-05 17:29:43 -08:00
parent a533ad2307
commit b00989a632
4 changed files with 70 additions and 73 deletions

View File

@@ -12,9 +12,9 @@ This table shows how the various MySQLdb and sqlite exceptions are mapped to a w
| `OperationalError` | *N/A* | `OperationalError` | 1049 | Open non-existent database |
| `DatabaseExists` | *N/A* | `ProgrammingError` | 1007 | Database already exists |
| `OperationalError` | `OperationalError` | `OperationalError` | 1050 | Table already exists |
| `ProgrammingError` | *N/A* | `ProgrammingError` | 1146 | SELECT on non-existing database |
| `ProgrammingError` | `OperationalError` | `ProgrammingError` | 1146 | SELECT non-existing table |
| `OperationalError` | `OperationalError` | `OperationalError` | 1054 | SELECT non-existing column |
| `ProgrammingError` | *N/A* | `ProgrammingError` | 1146 | SELECT on non-existing database |
| `IntegrityError` | `IntegrityError` | `IntegrityError` | 1062 | Duplicate key |
###V3.6 Exception hierarchy
@@ -42,9 +42,9 @@ StandardError
| `NoDatabaseError` | *N/A* | `OperationalError` | 1049 | Open non-existent database |
| `DatabaseExistsError` | *N/A* | `ProgrammingError` | 1007 | Database already exists |
| `TableExistsError` | `OperationalError` | `OperationalError` | 1050 | Table already exists |
| `NoTableError` | *N/A* | `ProgrammingError` | 1146 | SELECT on non-existing database |
| `NoTableError` | `OperationalError` | `ProgrammingError` | 1146 | SELECT non-existing table |
| `NoColumnError` | `OperationalError` | `OperationalError` | 1054 | SELECT non-existing column |
| `ProgrammingError` | *N/A* | `ProgrammingError` | 1146 | SELECT on non-existing database |
| `IntegrityError` | `IntegrityError` | `IntegrityError` | 1062 | Duplicate key |
###V3.7 Exception hierarchy

View File

@@ -1,32 +1,49 @@
#
# Copyright (c) 2009-2015 Tom Keffer <tkeffer@gmail.com>
# Copyright (c) 2009-2017 Tom Keffer <tkeffer@gmail.com>
#
# See the file LICENSE.txt for your full rights.
#
"""Driver for the MySQL database"""
"""weedb driver for the MySQL database"""
import decimal
import MySQLdb
from _mysql_exceptions import IntegrityError, ProgrammingError, OperationalError
from _mysql_exceptions import DatabaseError, IntegrityError, ProgrammingError, OperationalError
from weeutil.weeutil import to_bool
import weedb
DEFAULT_ENGINE = 'INNODB'
exception_map = {
1007: weedb.DatabaseExistsError,
1008: weedb.NoDatabaseError,
1044: weedb.PermissionError,
1045: weedb.BadPasswordError,
1049: weedb.NoDatabaseError,
1050: weedb.TableExistsError,
1054: weedb.NoColumnError,
1062: weedb.IntegrityError,
1146: weedb.NoTableError,
2002: weedb.CannotConnectError,
2005: weedb.CannotConnectError,
None: weedb.DatabaseError
}
def guard(fn):
"""Decorator function that converts MySQL exceptions into weedb exceptions."""
def guarded_fn(*args, **kwargs):
try:
return fn(*args, **kwargs)
except IntegrityError, e:
raise weedb.IntegrityError(e)
except ProgrammingError, e:
raise weedb.ProgrammingError(e)
except OperationalError, e:
raise weedb.OperationalError(e)
except DatabaseError, e:
# Default exception is weedb.DatabaseError
try:
errno = e[0]
except IndexError:
errno = None
klass = exception_map.get(errno, weedb.DatabaseError)
raise klass(e)
return guarded_fn
@@ -34,37 +51,26 @@ def guard(fn):
def connect(host='localhost', user='', password='', database_name='',
driver='', engine=DEFAULT_ENGINE, **kwargs):
"""Connect to the specified database"""
if host not in ('localhost', '127.0.0.1'):
kwargs.setdefault('port', 3306)
return Connection(host=host, user=user, password=password,
database_name=database_name, engine=engine, **kwargs)
def create(host='localhost', user='', password='', database_name='',
driver='', engine=DEFAULT_ENGINE, **kwargs):
"""Create the specified database. If it already exists,
an exception of type weedb.DatabaseExists will be thrown."""
an exception of type weedb.DatabaseExistsError will be thrown."""
# Open up a connection w/o specifying the database.
if host not in ('localhost', '127.0.0.1'):
kwargs.setdefault('port', 3306)
connect = Connection(host=host,
user=user,
password=password,
**kwargs)
cursor = connect.cursor()
try:
connect = MySQLdb.connect(host=host,
user=user,
passwd=password,
**kwargs)
set_engine(connect, engine)
cursor = connect.cursor()
# An exception will get thrown if the database already exists.
try:
# Now create the database.
cursor.execute("CREATE DATABASE %s" % (database_name,))
except ProgrammingError:
# The database already exists. Change the type of exception.
raise weedb.DatabaseExists("Database %s already exists" % (database_name,))
finally:
cursor.close()
connect.close()
except OperationalError, e:
raise weedb.OperationalError(e)
# Now create the database.
cursor.execute("CREATE DATABASE %s" % (database_name,))
finally:
cursor.close()
connect.close()
def drop(host='localhost', user='', password='', database_name='',
@@ -73,23 +79,19 @@ def drop(host='localhost', user='', password='', database_name='',
if host not in ('localhost', '127.0.0.1'):
kwargs.setdefault('port', 3306)
# Open up a connection
connect = Connection(host=host,
user=user,
password=password,
**kwargs)
cursor = connect.cursor()
try:
connect = MySQLdb.connect(host=host,
user=user,
passwd=password,
**kwargs)
cursor = connect.cursor()
try:
cursor.execute("DROP DATABASE %s" % database_name)
except OperationalError:
raise weedb.NoDatabase("""Attempt to drop non-existent database %s""" % (database_name,))
finally:
cursor.close()
connect.close()
except OperationalError, e:
raise weedb.OperationalError(e)
cursor.execute("DROP DATABASE %s" % database_name)
finally:
cursor.close()
connect.close()
@guard
class Connection(weedb.Connection):
"""A wrapper around a MySQL connection object."""
@@ -111,16 +113,9 @@ class Connection(weedb.Connection):
"""
if host not in ('localhost', '127.0.0.1'):
kwargs.setdefault('port', 3306)
try:
connection = MySQLdb.connect(host=host, user=user, passwd=password, db=database_name, **kwargs)
except OperationalError, e:
# The MySQL driver does not include the database in the
# exception information. Tack it on, in case it might be useful.
msg = str(e) + " while opening database '%s'" % (database_name,)
if e.args[0] == 2002:
raise weedb.CannotConnect(msg)
else:
raise weedb.OperationalError(msg)
connection = MySQLdb.connect(host=host, user=user, passwd=password,
db=database_name, **kwargs)
weedb.Connection.__init__(self, connection, database_name, 'mysql')
@@ -223,6 +218,7 @@ class Connection(weedb.Connection):
class Cursor(object):
"""A wrapper around the MySQLdb cursor object"""
@guard
def __init__(self, connection):
"""Initialize a Cursor from a connection.
@@ -251,9 +247,9 @@ class Cursor(object):
return self
def fetchone(self):
# Get a result from the MySQL cursor, then run it through the massage
# Get a result from the MySQL cursor, then run it through the _massage
# filter below
return massage(self.cursor.fetchone())
return _massage(self.cursor.fetchone())
def close(self):
try:
@@ -279,8 +275,8 @@ class Cursor(object):
# This is a utility function for converting a result set that might contain
# longs or decimal.Decimals (which MySQLdb uses) to something containing just ints.
#
def massage(seq):
# Return the massaged sequence if it exists, otherwise, return None
def _massage(seq):
# Return the _massaged sequence if it exists, otherwise, return None
if seq is not None:
return [int(i) if isinstance(i, long) or isinstance(i, decimal.Decimal) else i for i in seq]

View File

@@ -1,5 +1,7 @@
"""Test the weedb exception hierarchy"""
from __future__ import with_statement
import os
import stat
import unittest
import MySQLdb
@@ -47,22 +49,15 @@ class Cursor(object):
class Common(unittest.TestCase):
def setUp(self):
"""Drop the old databases, in preparation for running a test."""
try:
weedb.drop(mysql1_dict)
except weedb.NoDatabase:
pass
try:
weedb.drop(mysql2_dict)
except weedb.NoDatabase:
pass
try:
weedb.drop(sqdb1_dict)
except weedb.NoDatabase:
pass
try:
weedb.drop(sqdb2_dict)
except weedb.NoDatabase:
pass
def test_bad_host(self):
mysql_dict = dict(mysql1_dict)
@@ -86,7 +81,10 @@ class Common(unittest.TestCase):
weedb.create(mysql1_dict)
with self.assertRaises(weedb.PermissionError):
weedb.drop(mysql2_dict)
with self.assertRaises(weedb.PermissionError):
weedb.create(sqdb1_dict)
# Can't really test this one without setting up a file where
# we have no write permission
with self.assertRaises(weedb.NoDatabaseError):
weedb.drop(sqdb2_dict)
def test_create_nopermission(self):
@@ -114,7 +112,7 @@ class Common(unittest.TestCase):
mysql_dict.pop('database_name')
connect = weedb.connect(mysql_dict)
cursor = connect.cursor()
with self.assertRaises(weedb.NoDatabaseError):
with self.assertRaises(weedb.NoTableError):
cursor.execute("SELECT foo from test_weewx1.bar")
cursor.close()
connect.close()

View File

@@ -127,6 +127,9 @@ with option retry_login. Fixes issue #212.
The test suites now use dedicated users 'weewx1' and 'weewx2'. A shell script
has been included to setup these users.
A more formal exception hierarchy has been adopted for the internal
database library weedb. See weedb/NOTES.md
3.6.2 11/08/2016