Switch from MySQL-python to PyMySQL

As discussed in the Liberty Design Summit "Moving apps to Python 3"
cross-project workshop, the way forward in the near future is to
switch to the pure-python PyMySQL library as a default.

Added a special test environment to keep MySQL-python support.
Documentation modified.

https://etherpad.openstack.org/p/liberty-cross-project-python3

Change-Id: I12b32dc097a121bd43991bc38dd4d289b65e86c1
This commit is contained in:
Jeremy Stanley 2015-05-20 01:04:01 +00:00 committed by Victor Sergeyev
parent 910d40aa39
commit 9b552046f5
11 changed files with 188 additions and 100 deletions

View File

@ -26,12 +26,15 @@ How to run unit tests
oslo.db (as all OpenStack projects) uses tox to run unit tests. You can find oslo.db (as all OpenStack projects) uses tox to run unit tests. You can find
general information about OpenStack unit tests and testing with tox in wiki_. general information about OpenStack unit tests and testing with tox in wiki_.
oslo.db tests use MySQL-python as the default MySQL DB API driver (which is oslo.db tests use PyMySQL as the default MySQL DB API driver (which is true for
true for OpenStack), and psycopg2 for PostgreSQL. pip will build these libs in OpenStack), and psycopg2 for PostgreSQL. pip will build these libs in your
your venv, so you must ensure that you have the required system packages venv, so you must ensure that you have the required system packages installed
installed. For Ubuntu/Debian they are python-dev, libmysqlclient-dev and for psycopg2 (PyMySQL is a pure-Python implementation and so needs no
libpq-dev. For Fedora/CentOS - gcc, python-devel, postgresql-devel and additional system packages). For Ubuntu/Debian they are python-dev, and
mysql-devel. libpq-dev. For Fedora/CentOS - gcc, python-devel and postgresql-devel.
There is also a separate env for testing with MySQL-python. If you are suppose
to run these tests as well, you need to install libmysqlclient-dev on Ubuntu/Debian
or mysql-devel for Fedora/CentOS.
The oslo.db unit tests system allows to run unittests on real databases. At the The oslo.db unit tests system allows to run unittests on real databases. At the
moment it supports MySQL, PostgreSQL and SQLite. moment it supports MySQL, PostgreSQL and SQLite.

View File

@ -8,16 +8,37 @@ At the command line::
You will also need to install at least one SQL backend:: You will also need to install at least one SQL backend::
$ pip install MySQL-python $ pip install psycopg2
Or::
$ pip install PyMySQL
Or:: Or::
$ pip install pysqlite $ pip install pysqlite
Using with MySQL
----------------
If using MySQL make sure to install the MySQL client development package for Using with PostgreSQL
---------------------
If you are using PostgreSQL make sure to install the PostgreSQL client
development package for your distro. On Ubuntu this is done as follows::
$ sudo apt-get install libpq-dev
$ pip install psycopg2
The installation of psycopg2 will fail if libpq-dev is not installed first.
Note that even in a virtual environment the libpq-dev will be installed
system wide.
Using with MySQL-python
-----------------------
PyMySQL is a default MySQL DB API driver for oslo.db, as well as for the whole
OpenStack. But you still can use MySQL-python as an alternative DB API driver.
For MySQL-python you must install the MySQL client development package for
your distro. On Ubuntu this is done as follows:: your distro. On Ubuntu this is done as follows::
$ sudo apt-get install libmysqlclient-dev $ sudo apt-get install libmysqlclient-dev

View File

@ -455,7 +455,7 @@ class MySQLBackendImpl(BackendImpl):
default_engine_kwargs = {'mysql_sql_mode': 'TRADITIONAL'} default_engine_kwargs = {'mysql_sql_mode': 'TRADITIONAL'}
def create_opportunistic_driver_url(self): def create_opportunistic_driver_url(self):
return "mysql://openstack_citest:openstack_citest@localhost/" return "mysql+pymysql://openstack_citest:openstack_citest@localhost/"
def create_named_database(self, engine, ident, conditional=False): def create_named_database(self, engine, ident, conditional=False):
with engine.connect() as conn: with engine.connect() as conn:

View File

@ -46,7 +46,16 @@ class _SQLAExceptionMatcher(object):
self.assertTrue(issubclass(exc.__class__, exception_type)) self.assertTrue(issubclass(exc.__class__, exception_type))
else: else:
self.assertEqual(exc.__class__.__name__, exception_type) self.assertEqual(exc.__class__.__name__, exception_type)
self.assertEqual(str(exc.orig).lower(), message.lower()) if isinstance(message, tuple):
self.assertEqual(
[a.lower()
if isinstance(a, six.string_types) else a
for a in exc.orig.args],
[m.lower()
if isinstance(m, six.string_types) else m for m in message]
)
else:
self.assertEqual(str(exc.orig).lower(), message.lower())
if sql is not None: if sql is not None:
self.assertEqual(exc.statement, sql) self.assertEqual(exc.statement, sql)
if params is not None: if params is not None:
@ -359,10 +368,10 @@ class TestReferenceErrorMySQL(TestReferenceErrorSQLite,
self.assertInnerException( self.assertInnerException(
matched, matched,
"IntegrityError", "IntegrityError",
"(1452, 'Cannot add or update a child row: a " (1452, "Cannot add or update a child row: a "
"foreign key constraint fails (`{0}`.`resource_entity`, " "foreign key constraint fails (`{0}`.`resource_entity`, "
"CONSTRAINT `foo_fkey` FOREIGN KEY (`foo_id`) REFERENCES " "CONSTRAINT `foo_fkey` FOREIGN KEY (`foo_id`) REFERENCES "
"`resource_foo` (`id`))')".format(self.engine.url.database), "`resource_foo` (`id`))".format(self.engine.url.database)),
"INSERT INTO resource_entity (id, foo_id) VALUES (%s, %s)", "INSERT INTO resource_entity (id, foo_id) VALUES (%s, %s)",
(1, 2) (1, 2)
) )
@ -382,10 +391,13 @@ class TestReferenceErrorMySQL(TestReferenceErrorSQLite,
self.assertInnerException( self.assertInnerException(
matched, matched,
"IntegrityError", "IntegrityError",
'(1452, \'Cannot add or update a child row: a ' (
'foreign key constraint fails ("{0}"."resource_entity", ' 1452,
'CONSTRAINT "foo_fkey" FOREIGN KEY ("foo_id") REFERENCES ' 'Cannot add or update a child row: a '
'"resource_foo" ("id"))\')'.format(self.engine.url.database), 'foreign key constraint fails ("{0}"."resource_entity", '
'CONSTRAINT "foo_fkey" FOREIGN KEY ("foo_id") REFERENCES '
'"resource_foo" ("id"))'.format(self.engine.url.database)
),
"INSERT INTO resource_entity (id, foo_id) VALUES (%s, %s)", "INSERT INTO resource_entity (id, foo_id) VALUES (%s, %s)",
(1, 2) (1, 2)
) )
@ -436,7 +448,7 @@ class TestDuplicate(TestsExceptionFilter):
"PRIMARY KEY must be unique 'insert into t values(10)'", "PRIMARY KEY must be unique 'insert into t values(10)'",
expected_columns=[]) expected_columns=[])
def test_mysql_mysqldb(self): def test_mysql_pymysql(self):
self._run_dupe_constraint_test( self._run_dupe_constraint_test(
"mysql", "mysql",
'(1062, "Duplicate entry ' '(1062, "Duplicate entry '
@ -541,7 +553,7 @@ class TestDeadlock(TestsExceptionFilter):
self.assertEqual(matched.orig.__class__.__name__, expected_dbapi_cls) self.assertEqual(matched.orig.__class__.__name__, expected_dbapi_cls)
def test_mysql_mysqldb_deadlock(self): def test_mysql_pymysql_deadlock(self):
self._run_deadlock_detect_test( self._run_deadlock_detect_test(
"mysql", "mysql",
"(1213, 'Deadlock found when trying " "(1213, 'Deadlock found when trying "
@ -549,7 +561,7 @@ class TestDeadlock(TestsExceptionFilter):
"transaction')" "transaction')"
) )
def test_mysql_mysqldb_galera_deadlock(self): def test_mysql_pymysql_galera_deadlock(self):
self._run_deadlock_detect_test( self._run_deadlock_detect_test(
"mysql", "mysql",
"(1205, 'Lock wait timeout exceeded; " "(1205, 'Lock wait timeout exceeded; "

View File

@ -793,8 +793,10 @@ class TestDialectFunctionDispatcher(test_base.BaseTestCase):
dispatcher = orig = utils.dispatch_for_dialect("*")( dispatcher = orig = utils.dispatch_for_dialect("*")(
callable_fn.default) callable_fn.default)
dispatcher = dispatcher.dispatch_for("sqlite")(callable_fn.sqlite) dispatcher = dispatcher.dispatch_for("sqlite")(callable_fn.sqlite)
dispatcher = dispatcher.dispatch_for("mysql+mysqldb")( dispatcher = dispatcher.dispatch_for("mysql+pymysql")(
callable_fn.mysql_mysqldb) callable_fn.mysql_pymysql)
dispatcher = dispatcher.dispatch_for("mysql")(
callable_fn.mysql)
dispatcher = dispatcher.dispatch_for("postgresql")( dispatcher = dispatcher.dispatch_for("postgresql")(
callable_fn.postgresql) callable_fn.postgresql)
@ -808,7 +810,8 @@ class TestDialectFunctionDispatcher(test_base.BaseTestCase):
for targ in [ for targ in [
callable_fn.default, callable_fn.default,
callable_fn.sqlite, callable_fn.sqlite,
callable_fn.mysql_mysqldb, callable_fn.mysql,
callable_fn.mysql_pymysql,
callable_fn.postgresql, callable_fn.postgresql,
callable_fn.postgresql_psycopg2, callable_fn.postgresql_psycopg2,
callable_fn.pyodbc callable_fn.pyodbc
@ -818,8 +821,10 @@ class TestDialectFunctionDispatcher(test_base.BaseTestCase):
dispatcher = orig = utils.dispatch_for_dialect("*", multiple=True)( dispatcher = orig = utils.dispatch_for_dialect("*", multiple=True)(
callable_fn.default) callable_fn.default)
dispatcher = dispatcher.dispatch_for("sqlite")(callable_fn.sqlite) dispatcher = dispatcher.dispatch_for("sqlite")(callable_fn.sqlite)
dispatcher = dispatcher.dispatch_for("mysql+mysqldb")( dispatcher = dispatcher.dispatch_for("mysql+pymysql")(
callable_fn.mysql_mysqldb) callable_fn.mysql_pymysql)
dispatcher = dispatcher.dispatch_for("mysql")(
callable_fn.mysql)
dispatcher = dispatcher.dispatch_for("postgresql+*")( dispatcher = dispatcher.dispatch_for("postgresql+*")(
callable_fn.postgresql) callable_fn.postgresql)
dispatcher = dispatcher.dispatch_for("postgresql+psycopg2")( dispatcher = dispatcher.dispatch_for("postgresql+psycopg2")(
@ -836,15 +841,17 @@ class TestDialectFunctionDispatcher(test_base.BaseTestCase):
dispatcher, callable_fn = self._single_fixture() dispatcher, callable_fn = self._single_fixture()
dispatcher("sqlite://", 1) dispatcher("sqlite://", 1)
dispatcher("postgresql+psycopg2://u:p@h/t", 2) dispatcher("postgresql+psycopg2://u:p@h/t", 2)
dispatcher("mysql://u:p@h/t", 3) dispatcher("mysql+pymysql://u:p@h/t", 3)
dispatcher("mysql+mysqlconnector://u:p@h/t", 4) dispatcher("mysql://u:p@h/t", 4)
dispatcher("mysql+mysqlconnector://u:p@h/t", 5)
self.assertEqual( self.assertEqual(
[ [
mock.call.sqlite('sqlite://', 1), mock.call.sqlite('sqlite://', 1),
mock.call.postgresql("postgresql+psycopg2://u:p@h/t", 2), mock.call.postgresql("postgresql+psycopg2://u:p@h/t", 2),
mock.call.mysql_mysqldb("mysql://u:p@h/t", 3), mock.call.mysql_pymysql("mysql+pymysql://u:p@h/t", 3),
mock.call.default("mysql+mysqlconnector://u:p@h/t", 4) mock.call.mysql("mysql://u:p@h/t", 4),
mock.call.mysql("mysql+mysqlconnector://u:p@h/t", 5),
], ],
callable_fn.mock_calls) callable_fn.mock_calls)
@ -962,10 +969,10 @@ class TestDialectFunctionDispatcher(test_base.BaseTestCase):
def test_single_retval(self): def test_single_retval(self):
dispatcher, callable_fn = self._single_fixture() dispatcher, callable_fn = self._single_fixture()
callable_fn.mysql_mysqldb.return_value = 5 callable_fn.mysql_pymysql.return_value = 5
self.assertEqual( self.assertEqual(
dispatcher("mysql://u:p@h/t", 3), 5 dispatcher("mysql+pymysql://u:p@h/t", 3), 5
) )
def test_engine(self): def test_engine(self):
@ -978,14 +985,25 @@ class TestDialectFunctionDispatcher(test_base.BaseTestCase):
callable_fn.mock_calls callable_fn.mock_calls
) )
def test_url(self): def test_url_pymysql(self):
url = sqlalchemy.engine.url.make_url( url = sqlalchemy.engine.url.make_url(
"mysql+mysqldb://scott:tiger@localhost/test") "mysql+pymysql://scott:tiger@localhost/test")
dispatcher, callable_fn = self._single_fixture() dispatcher, callable_fn = self._single_fixture()
dispatcher(url, 15) dispatcher(url, 15)
self.assertEqual( self.assertEqual(
[mock.call.mysql_mysqldb(url, 15)], [mock.call.mysql_pymysql(url, 15)],
callable_fn.mock_calls
)
def test_url_mysql_generic(self):
url = sqlalchemy.engine.url.make_url(
"mysql://scott:tiger@localhost/test")
dispatcher, callable_fn = self._single_fixture()
dispatcher(url, 15)
self.assertEqual(
[mock.call.mysql(url, 15)],
callable_fn.mock_calls callable_fn.mock_calls
) )
@ -1030,9 +1048,10 @@ class TestDialectFunctionDispatcher(test_base.BaseTestCase):
dispatcher, callable_fn = self._multiple_fixture() dispatcher, callable_fn = self._multiple_fixture()
dispatcher("postgresql+pyodbc://", 1) dispatcher("postgresql+pyodbc://", 1)
dispatcher("mysql://", 2) dispatcher("mysql+pymysql://", 2)
dispatcher("ibm_db_sa+db2://", 3) dispatcher("ibm_db_sa+db2://", 3)
dispatcher("postgresql+psycopg2://", 4) dispatcher("postgresql+psycopg2://", 4)
dispatcher("postgresql://", 5)
# TODO(zzzeek): there is a deterministic order here, but we might # TODO(zzzeek): there is a deterministic order here, but we might
# want to tweak it, or maybe provide options. default first? # want to tweak it, or maybe provide options. default first?
@ -1042,12 +1061,18 @@ class TestDialectFunctionDispatcher(test_base.BaseTestCase):
mock.call.postgresql('postgresql+pyodbc://', 1), mock.call.postgresql('postgresql+pyodbc://', 1),
mock.call.pyodbc('postgresql+pyodbc://', 1), mock.call.pyodbc('postgresql+pyodbc://', 1),
mock.call.default('postgresql+pyodbc://', 1), mock.call.default('postgresql+pyodbc://', 1),
mock.call.mysql_mysqldb('mysql://', 2), mock.call.mysql_pymysql('mysql+pymysql://', 2),
mock.call.default('mysql://', 2), mock.call.mysql('mysql+pymysql://', 2),
mock.call.default('mysql+pymysql://', 2),
mock.call.default('ibm_db_sa+db2://', 3), mock.call.default('ibm_db_sa+db2://', 3),
mock.call.postgresql_psycopg2('postgresql+psycopg2://', 4), mock.call.postgresql_psycopg2('postgresql+psycopg2://', 4),
mock.call.postgresql('postgresql+psycopg2://', 4), mock.call.postgresql('postgresql+psycopg2://', 4),
mock.call.default('postgresql+psycopg2://', 4), mock.call.default('postgresql+psycopg2://', 4),
# note this is called because we resolve the default
# DBAPI for the url
mock.call.postgresql_psycopg2('postgresql://', 5),
mock.call.postgresql('postgresql://', 5),
mock.call.default('postgresql://', 5),
], ],
callable_fn.mock_calls callable_fn.mock_calls
) )

View File

@ -46,7 +46,16 @@ class _SQLAExceptionMatcher(object):
self.assertTrue(issubclass(exc.__class__, exception_type)) self.assertTrue(issubclass(exc.__class__, exception_type))
else: else:
self.assertEqual(exc.__class__.__name__, exception_type) self.assertEqual(exc.__class__.__name__, exception_type)
self.assertEqual(str(exc.orig).lower(), message.lower()) if isinstance(message, tuple):
self.assertEqual(
[a.lower()
if isinstance(a, six.string_types) else a
for a in exc.orig.args],
[m.lower()
if isinstance(m, six.string_types) else m for m in message]
)
else:
self.assertEqual(str(exc.orig).lower(), message.lower())
if sql is not None: if sql is not None:
self.assertEqual(exc.statement, sql) self.assertEqual(exc.statement, sql)
if params is not None: if params is not None:
@ -362,10 +371,10 @@ class TestReferenceErrorMySQL(TestReferenceErrorSQLite,
self.assertInnerException( self.assertInnerException(
matched, matched,
"IntegrityError", "IntegrityError",
"(1452, 'Cannot add or update a child row: a " (1452, "Cannot add or update a child row: a "
"foreign key constraint fails (`{0}`.`resource_entity`, " "foreign key constraint fails (`{0}`.`resource_entity`, "
"CONSTRAINT `foo_fkey` FOREIGN KEY (`foo_id`) REFERENCES " "CONSTRAINT `foo_fkey` FOREIGN KEY (`foo_id`) REFERENCES "
"`resource_foo` (`id`))')".format(self.engine.url.database), "`resource_foo` (`id`))".format(self.engine.url.database)),
"INSERT INTO resource_entity (id, foo_id) VALUES (%s, %s)", "INSERT INTO resource_entity (id, foo_id) VALUES (%s, %s)",
(1, 2) (1, 2)
) )
@ -390,10 +399,13 @@ class TestReferenceErrorMySQL(TestReferenceErrorSQLite,
self.assertInnerException( self.assertInnerException(
matched, matched,
"IntegrityError", "IntegrityError",
'(1452, \'Cannot add or update a child row: a ' (
'foreign key constraint fails ("{0}"."resource_entity", ' 1452,
'CONSTRAINT "foo_fkey" FOREIGN KEY ("foo_id") REFERENCES ' 'Cannot add or update a child row: a '
'"resource_foo" ("id"))\')'.format(self.engine.url.database), 'foreign key constraint fails ("{0}"."resource_entity", '
'CONSTRAINT "foo_fkey" FOREIGN KEY ("foo_id") REFERENCES '
'"resource_foo" ("id"))'.format(self.engine.url.database)
),
"INSERT INTO resource_entity (id, foo_id) VALUES (%s, %s)", "INSERT INTO resource_entity (id, foo_id) VALUES (%s, %s)",
(1, 2) (1, 2)
) )
@ -414,11 +426,14 @@ class TestReferenceErrorMySQL(TestReferenceErrorSQLite,
self.assertInnerException( self.assertInnerException(
matched, matched,
"IntegrityError", "IntegrityError",
"(1451, 'cannot delete or update a parent row: a foreign key " (
"constraint fails (`{0}`.`resource_entity`, " 1451,
"constraint `foo_fkey` " "Cannot delete or update a parent row: a foreign key "
"foreign key (`foo_id`) references " "constraint fails (`{0}`.`resource_entity`, "
"`resource_foo` (`id`))')".format(self.engine.url.database), "constraint `foo_fkey` "
"foreign key (`foo_id`) references "
"`resource_foo` (`id`))".format(self.engine.url.database)
),
"DELETE FROM resource_foo", "DELETE FROM resource_foo",
(), (),
) )
@ -483,7 +498,7 @@ class TestDuplicate(TestsExceptionFilter):
"PRIMARY KEY must be unique 'insert into t values(10)'", "PRIMARY KEY must be unique 'insert into t values(10)'",
expected_columns=[]) expected_columns=[])
def test_mysql_mysqldb(self): def test_mysql_pymysql(self):
self._run_dupe_constraint_test( self._run_dupe_constraint_test(
"mysql", "mysql",
'(1062, "Duplicate entry ' '(1062, "Duplicate entry '
@ -606,7 +621,7 @@ class TestDeadlock(TestsExceptionFilter):
self.assertEqual(matched.orig.__class__.__name__, expected_dbapi_cls) self.assertEqual(matched.orig.__class__.__name__, expected_dbapi_cls)
def test_mysql_mysqldb_deadlock(self): def test_mysql_pymysql_deadlock(self):
self._run_deadlock_detect_test( self._run_deadlock_detect_test(
"mysql", "mysql",
"(1213, 'Deadlock found when trying " "(1213, 'Deadlock found when trying "
@ -614,7 +629,7 @@ class TestDeadlock(TestsExceptionFilter):
"transaction')" "transaction')"
) )
def test_mysql_mysqldb_galera_deadlock(self): def test_mysql_pymysql_galera_deadlock(self):
self._run_deadlock_detect_test( self._run_deadlock_detect_test(
"mysql", "mysql",
"(1205, 'Lock wait timeout exceeded; " "(1205, 'Lock wait timeout exceeded; "

View File

@ -558,7 +558,7 @@ class CreateEngineTest(oslo_test.BaseTestCase):
def test_queuepool_args(self): def test_queuepool_args(self):
engines._init_connection_args( engines._init_connection_args(
url.make_url("mysql://u:p@host/test"), self.args, url.make_url("mysql+pymysql://u:p@host/test"), self.args,
max_pool_size=10, max_overflow=10) max_pool_size=10, max_overflow=10)
self.assertEqual(self.args['pool_size'], 10) self.assertEqual(self.args['pool_size'], 10)
self.assertEqual(self.args['max_overflow'], 10) self.assertEqual(self.args['max_overflow'], 10)
@ -609,6 +609,12 @@ class CreateEngineTest(oslo_test.BaseTestCase):
self.assertEqual(self.args['connect_args'], self.assertEqual(self.args['connect_args'],
{'charset': 'utf8', 'use_unicode': 0}) {'charset': 'utf8', 'use_unicode': 0})
def test_mysql_pymysql_connect_args_default(self):
engines._init_connection_args(
url.make_url("mysql+pymysql://u:p@host/test"), self.args)
self.assertEqual(self.args['connect_args'],
{'charset': 'utf8'})
def test_mysql_mysqldb_connect_args_default(self): def test_mysql_mysqldb_connect_args_default(self):
engines._init_connection_args( engines._init_connection_args(
url.make_url("mysql+mysqldb://u:p@host/test"), self.args) url.make_url("mysql+mysqldb://u:p@host/test"), self.args)

View File

@ -912,8 +912,10 @@ class TestDialectFunctionDispatcher(test_base.BaseTestCase):
dispatcher = orig = utils.dispatch_for_dialect("*")( dispatcher = orig = utils.dispatch_for_dialect("*")(
callable_fn.default) callable_fn.default)
dispatcher = dispatcher.dispatch_for("sqlite")(callable_fn.sqlite) dispatcher = dispatcher.dispatch_for("sqlite")(callable_fn.sqlite)
dispatcher = dispatcher.dispatch_for("mysql+mysqldb")( dispatcher = dispatcher.dispatch_for("mysql+pymysql")(
callable_fn.mysql_mysqldb) callable_fn.mysql_pymysql)
dispatcher = dispatcher.dispatch_for("mysql")(
callable_fn.mysql)
dispatcher = dispatcher.dispatch_for("postgresql")( dispatcher = dispatcher.dispatch_for("postgresql")(
callable_fn.postgresql) callable_fn.postgresql)
@ -927,7 +929,8 @@ class TestDialectFunctionDispatcher(test_base.BaseTestCase):
for targ in [ for targ in [
callable_fn.default, callable_fn.default,
callable_fn.sqlite, callable_fn.sqlite,
callable_fn.mysql_mysqldb, callable_fn.mysql,
callable_fn.mysql_pymysql,
callable_fn.postgresql, callable_fn.postgresql,
callable_fn.postgresql_psycopg2, callable_fn.postgresql_psycopg2,
callable_fn.pyodbc callable_fn.pyodbc
@ -937,8 +940,10 @@ class TestDialectFunctionDispatcher(test_base.BaseTestCase):
dispatcher = orig = utils.dispatch_for_dialect("*", multiple=True)( dispatcher = orig = utils.dispatch_for_dialect("*", multiple=True)(
callable_fn.default) callable_fn.default)
dispatcher = dispatcher.dispatch_for("sqlite")(callable_fn.sqlite) dispatcher = dispatcher.dispatch_for("sqlite")(callable_fn.sqlite)
dispatcher = dispatcher.dispatch_for("mysql+mysqldb")( dispatcher = dispatcher.dispatch_for("mysql+pymysql")(
callable_fn.mysql_mysqldb) callable_fn.mysql_pymysql)
dispatcher = dispatcher.dispatch_for("mysql")(
callable_fn.mysql)
dispatcher = dispatcher.dispatch_for("postgresql+*")( dispatcher = dispatcher.dispatch_for("postgresql+*")(
callable_fn.postgresql) callable_fn.postgresql)
dispatcher = dispatcher.dispatch_for("postgresql+psycopg2")( dispatcher = dispatcher.dispatch_for("postgresql+psycopg2")(
@ -955,15 +960,17 @@ class TestDialectFunctionDispatcher(test_base.BaseTestCase):
dispatcher, callable_fn = self._single_fixture() dispatcher, callable_fn = self._single_fixture()
dispatcher("sqlite://", 1) dispatcher("sqlite://", 1)
dispatcher("postgresql+psycopg2://u:p@h/t", 2) dispatcher("postgresql+psycopg2://u:p@h/t", 2)
dispatcher("mysql://u:p@h/t", 3) dispatcher("mysql+pymysql://u:p@h/t", 3)
dispatcher("mysql+mysqlconnector://u:p@h/t", 4) dispatcher("mysql://u:p@h/t", 4)
dispatcher("mysql+mysqlconnector://u:p@h/t", 5)
self.assertEqual( self.assertEqual(
[ [
mock.call.sqlite('sqlite://', 1), mock.call.sqlite('sqlite://', 1),
mock.call.postgresql("postgresql+psycopg2://u:p@h/t", 2), mock.call.postgresql("postgresql+psycopg2://u:p@h/t", 2),
mock.call.mysql_mysqldb("mysql://u:p@h/t", 3), mock.call.mysql_pymysql("mysql+pymysql://u:p@h/t", 3),
mock.call.default("mysql+mysqlconnector://u:p@h/t", 4) mock.call.mysql("mysql://u:p@h/t", 4),
mock.call.mysql("mysql+mysqlconnector://u:p@h/t", 5),
], ],
callable_fn.mock_calls) callable_fn.mock_calls)
@ -1081,10 +1088,10 @@ class TestDialectFunctionDispatcher(test_base.BaseTestCase):
def test_single_retval(self): def test_single_retval(self):
dispatcher, callable_fn = self._single_fixture() dispatcher, callable_fn = self._single_fixture()
callable_fn.mysql_mysqldb.return_value = 5 callable_fn.mysql_pymysql.return_value = 5
self.assertEqual( self.assertEqual(
dispatcher("mysql://u:p@h/t", 3), 5 dispatcher("mysql+pymysql://u:p@h/t", 3), 5
) )
def test_engine(self): def test_engine(self):
@ -1097,14 +1104,25 @@ class TestDialectFunctionDispatcher(test_base.BaseTestCase):
callable_fn.mock_calls callable_fn.mock_calls
) )
def test_url(self): def test_url_pymysql(self):
url = sqlalchemy.engine.url.make_url( url = sqlalchemy.engine.url.make_url(
"mysql+mysqldb://scott:tiger@localhost/test") "mysql+pymysql://scott:tiger@localhost/test")
dispatcher, callable_fn = self._single_fixture() dispatcher, callable_fn = self._single_fixture()
dispatcher(url, 15) dispatcher(url, 15)
self.assertEqual( self.assertEqual(
[mock.call.mysql_mysqldb(url, 15)], [mock.call.mysql_pymysql(url, 15)],
callable_fn.mock_calls
)
def test_url_mysql_generic(self):
url = sqlalchemy.engine.url.make_url(
"mysql://scott:tiger@localhost/test")
dispatcher, callable_fn = self._single_fixture()
dispatcher(url, 15)
self.assertEqual(
[mock.call.mysql(url, 15)],
callable_fn.mock_calls callable_fn.mock_calls
) )
@ -1149,9 +1167,10 @@ class TestDialectFunctionDispatcher(test_base.BaseTestCase):
dispatcher, callable_fn = self._multiple_fixture() dispatcher, callable_fn = self._multiple_fixture()
dispatcher("postgresql+pyodbc://", 1) dispatcher("postgresql+pyodbc://", 1)
dispatcher("mysql://", 2) dispatcher("mysql+pymysql://", 2)
dispatcher("ibm_db_sa+db2://", 3) dispatcher("ibm_db_sa+db2://", 3)
dispatcher("postgresql+psycopg2://", 4) dispatcher("postgresql+psycopg2://", 4)
dispatcher("postgresql://", 5)
# TODO(zzzeek): there is a deterministic order here, but we might # TODO(zzzeek): there is a deterministic order here, but we might
# want to tweak it, or maybe provide options. default first? # want to tweak it, or maybe provide options. default first?
@ -1161,12 +1180,18 @@ class TestDialectFunctionDispatcher(test_base.BaseTestCase):
mock.call.postgresql('postgresql+pyodbc://', 1), mock.call.postgresql('postgresql+pyodbc://', 1),
mock.call.pyodbc('postgresql+pyodbc://', 1), mock.call.pyodbc('postgresql+pyodbc://', 1),
mock.call.default('postgresql+pyodbc://', 1), mock.call.default('postgresql+pyodbc://', 1),
mock.call.mysql_mysqldb('mysql://', 2), mock.call.mysql_pymysql('mysql+pymysql://', 2),
mock.call.default('mysql://', 2), mock.call.mysql('mysql+pymysql://', 2),
mock.call.default('mysql+pymysql://', 2),
mock.call.default('ibm_db_sa+db2://', 3), mock.call.default('ibm_db_sa+db2://', 3),
mock.call.postgresql_psycopg2('postgresql+psycopg2://', 4), mock.call.postgresql_psycopg2('postgresql+psycopg2://', 4),
mock.call.postgresql('postgresql+psycopg2://', 4), mock.call.postgresql('postgresql+psycopg2://', 4),
mock.call.default('postgresql+psycopg2://', 4), mock.call.default('postgresql+psycopg2://', 4),
# note this is called because we resolve the default
# DBAPI for the url
mock.call.postgresql_psycopg2('postgresql://', 5),
mock.call.postgresql('postgresql://', 5),
mock.call.default('postgresql://', 5),
], ],
callable_fn.mock_calls callable_fn.mock_calls
) )

View File

@ -1,19 +0,0 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
hacking>=0.10.0,<0.11
coverage>=3.6
discover
doc8 # Apache-2.0
fixtures>=0.3.14
MySQL-python
psycopg2
python-subunit>=0.0.18
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
oslosphinx>=2.5.0 # Apache-2.0
oslotest>=1.5.1 # Apache-2.0
testrepository>=0.0.18
testtools>=0.9.36,!=1.2.0
tempest-lib>=0.5.0

View File

@ -8,12 +8,12 @@ coverage>=3.6
discover discover
doc8 # Apache-2.0 doc8 # Apache-2.0
fixtures>=0.3.14 fixtures>=0.3.14
PyMySQL>=0.6.2 # MIT License
psycopg2 psycopg2
python-subunit>=0.0.18 python-subunit>=0.0.18
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
oslosphinx>=2.5.0 # Apache-2.0 oslosphinx>=2.5.0 # Apache-2.0
oslotest>=1.5.1 # Apache-2.0 oslotest>=1.5.1 # Apache-2.0
PyMySQL>=0.6.2 # MIT License
testrepository>=0.0.18 testrepository>=0.0.18
testtools>=0.9.36,!=1.2.0 testtools>=0.9.36,!=1.2.0
tempest-lib>=0.5.0 tempest-lib>=0.5.0

10
tox.ini
View File

@ -14,7 +14,7 @@ install_command = pip install -U {opts} {packages}
setenv = setenv =
VIRTUAL_ENV={envdir} VIRTUAL_ENV={envdir}
deps = -r{toxinidir}/requirements.txt deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements-py2.txt -r{toxinidir}/test-requirements.txt
commands = bash tools/pretty_tox.sh '{posargs}' commands = bash tools/pretty_tox.sh '{posargs}'
[testenv:sqla_09] [testenv:sqla_09]
@ -25,12 +25,12 @@ commands = pip install SQLAlchemy>=0.9.0,!=0.9.5,<1.0.0
commands = pip install SQLAlchemy>=0.8.0,<0.9.0 commands = pip install SQLAlchemy>=0.8.0,<0.9.0
python setup.py testr --slowest --testr-args='{posargs}' python setup.py testr --slowest --testr-args='{posargs}'
[testenv:py34] [testenv:mysql-python]
setenv = setenv =
{[testenv]setenv} {[testenv]setenv}
OS_TEST_DBAPI_ADMIN_CONNECTION=mysql+pymysql://openstack_citest:openstack_citest@localhost/;postgresql://openstack_citest:openstack_citest@localhost/postgres;sqlite:// OS_TEST_DBAPI_ADMIN_CONNECTION=mysql://openstack_citest:openstack_citest@localhost/;postgresql://openstack_citest:openstack_citest@localhost/postgres;sqlite://
deps = -r{toxinidir}/requirements.txt commands = pip install MySQL-python
-r{toxinidir}/test-requirements-py3.txt python setup.py testr --slowest --testr-args='{posargs}'
[testenv:pep8] [testenv:pep8]
commands = flake8 commands = flake8