From: Mike Bayer Date: Wed, 26 Jan 2011 16:18:03 +0000 (-0500) Subject: - New DBAPI support for pymysql, a pure Python port X-Git-Tag: rel_0_7b1~50 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=1515073b960b2319bd77ed6e9f6e04e458636a1e;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - New DBAPI support for pymysql, a pure Python port of MySQL-python. [ticket:1991] --- diff --git a/CHANGES b/CHANGES index 1c92414786..963addd967 100644 --- a/CHANGES +++ b/CHANGES @@ -166,6 +166,10 @@ CHANGES VARCHAR type which is similarly unbounded when no length specified. +- mysql + - New DBAPI support for pymysql, a pure Python port + of MySQL-python. [ticket:1991] + - drizzle - New dialect for Drizzle, a MySQL variant. Uses MySQL-python for the DBAPI. [ticket:2003] diff --git a/doc/build/core/engines.rst b/doc/build/core/engines.rst index 082b50a219..2d135f459d 100644 --- a/doc/build/core/engines.rst +++ b/doc/build/core/engines.rst @@ -62,7 +62,7 @@ Driver Connect string Py2K Py3K **DB2/Informix IDS** ibm-db_ thirdparty thirdparty thirdparty thirdparty thirdparty thirdparty **Drizzle** -drizzle_ ``drizzle+mysqldb``\* yes development no yes yes +mysql-python_ ``drizzle+mysqldb``\* yes development no yes yes **Firebird** kinterbasdb_ ``firebird+kinterbasdb``\* yes development no yes yes **Informix** @@ -82,6 +82,7 @@ pymssql_ ``mssql+pymssql`` yes development `MySQL Connector/Python`_ ``mysql+mysqlconnector`` yes yes no yes yes mysql-python_ ``mysql+mysqldb``\* yes development no yes yes OurSQL_ ``mysql+oursql`` yes yes no yes yes +pymysql_ ``mysql+pymysql`` yes development no yes yes **Oracle** cx_oracle_ ``oracle+cx_oracle``\* yes development no yes yes `Oracle JDBC Driver`_ ``oracle+zxjdbc`` no no yes yes yes @@ -106,6 +107,7 @@ python-sybase_ ``sybase+pysybase`` yes [1]_ development .. _mysql-python: http://sourceforge.net/projects/mysql-python .. _MySQL Connector/Python: https://launchpad.net/myconnpy .. _OurSQL: http://packages.python.org/oursql/ +.. _pymysql: http://code.google.com/p/pymysql/ .. _PostgreSQL JDBC Driver: http://jdbc.postgresql.org/ .. _sqlite3: http://docs.python.org/library/sqlite3.html .. _pysqlite: http://pypi.python.org/pypi/pysqlite/ @@ -123,7 +125,6 @@ python-sybase_ ``sybase+pysybase`` yes [1]_ development .. _informixdb: http://informixdb.sourceforge.net/ .. _sapdb: http://www.sapdb.org/sapdbapi.html .. _python-sybase: http://python-sybase.sourceforge.net/ -.. _drizzle: http://drizzle.org/ Further detail on dialects is available at :ref:`dialect_toplevel`. diff --git a/doc/build/dialects/mysql.rst b/doc/build/dialects/mysql.rst index 8796adb3e3..c8edbceab9 100644 --- a/doc/build/dialects/mysql.rst +++ b/doc/build/dialects/mysql.rst @@ -162,6 +162,11 @@ OurSQL Notes .. automodule:: sqlalchemy.dialects.mysql.oursql +pymysql Notes +------------- + +.. automodule:: sqlalchemy.dialects.mysql.pymysql + MySQL-Connector Notes ---------------------- diff --git a/lib/sqlalchemy/connectors/mysqldb.py b/lib/sqlalchemy/connectors/mysqldb.py index 7696cdb797..59744f2284 100644 --- a/lib/sqlalchemy/connectors/mysqldb.py +++ b/lib/sqlalchemy/connectors/mysqldb.py @@ -48,6 +48,7 @@ class MySQLDBConnector(Connector): @classmethod def dbapi(cls): + # is overridden when pymysql is used return __import__('MySQLdb') def do_executemany(self, cursor, statement, parameters, context=None): @@ -87,7 +88,9 @@ class MySQLDBConnector(Connector): client_flag = opts.get('client_flag', 0) if self.dbapi is not None: try: - from MySQLdb.constants import CLIENT as CLIENT_FLAGS + CLIENT_FLAGS = __import__(self.dbapi.__package__+'.constants', + globals(), locals(), + ['CLIENT'], 0).CLIENT client_flag |= CLIENT_FLAGS.FOUND_ROWS except: pass diff --git a/lib/sqlalchemy/dialects/drizzle/base.py b/lib/sqlalchemy/dialects/drizzle/base.py index 5c268fb603..bec0562a95 100644 --- a/lib/sqlalchemy/dialects/drizzle/base.py +++ b/lib/sqlalchemy/dialects/drizzle/base.py @@ -1,8 +1,6 @@ -# -*- fill-column: 78 -*- # drizzle/base.py -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Michael Bayer mike_mp@zzzcomputing.com -# and Jason Kirtland. -# Copyright (C) 2010 Monty Taylor +# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors +# Copyright (C) 2010-2011 Monty Taylor # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php diff --git a/lib/sqlalchemy/dialects/mysql/__init__.py b/lib/sqlalchemy/dialects/mysql/__init__.py index fe1ef49b26..7cab573e3e 100644 --- a/lib/sqlalchemy/dialects/mysql/__init__.py +++ b/lib/sqlalchemy/dialects/mysql/__init__.py @@ -5,7 +5,7 @@ # the MIT License: http://www.opensource.org/licenses/mit-license.php from sqlalchemy.dialects.mysql import base, mysqldb, oursql, \ - pyodbc, zxjdbc, mysqlconnector + pyodbc, zxjdbc, mysqlconnector, pymysql # default dialect base.dialect = mysqldb.dialect diff --git a/lib/sqlalchemy/dialects/mysql/mysqldb.py b/lib/sqlalchemy/dialects/mysql/mysqldb.py index 003502b13c..1b0ea85cb9 100644 --- a/lib/sqlalchemy/dialects/mysql/mysqldb.py +++ b/lib/sqlalchemy/dialects/mysql/mysqldb.py @@ -41,10 +41,11 @@ strings, also pass ``use_unicode=0`` in the connection arguments:: Known Issues ------------- -MySQL-python at least as of version 1.2.2 has a serious memory leak related +MySQL-python version 1.2.2 has a serious memory leak related to unicode conversion, a feature which is disabled via ``use_unicode=0``. -The recommended connection form with SQLAlchemy is:: - +Using a more recent version of MySQL-python is recommended. The +recommended connection form with SQLAlchemy is:: + engine = create_engine('mysql://scott:tiger@localhost/test?charset=utf8&use_unicode=0', pool_recycle=3600) diff --git a/lib/sqlalchemy/dialects/mysql/pymysql.py b/lib/sqlalchemy/dialects/mysql/pymysql.py new file mode 100644 index 0000000000..dee3dfeaa1 --- /dev/null +++ b/lib/sqlalchemy/dialects/mysql/pymysql.py @@ -0,0 +1,38 @@ +# mysql/pymysql.py +# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""Support for the MySQL database via the pymysql adapter. + +pymysql is available at: + + http://code.google.com/p/pymysql/ + +Connecting +---------- + +Connect string:: + + mysql+pymysql://:@/[?] + +MySQL-Python Compatibility +-------------------------- + +The pymysql DBAPI is a pure Python port of the MySQL-python (MySQLdb) driver, +and targets 100% compatibility. Most behavioral notes for MySQL-python apply to +the pymysql driver as well. + +""" + +from sqlalchemy.dialects.mysql.mysqldb import MySQLDialect_mysqldb + +class MySQLDialect_pymysql(MySQLDialect_mysqldb): + driver = 'pymysql' + + @classmethod + def dbapi(cls): + return __import__('pymysql') + +dialect = MySQLDialect_pymysql \ No newline at end of file diff --git a/test/aaa_profiling/test_orm.py b/test/aaa_profiling/test_orm.py index 7c89d22fd4..d028fa715e 100644 --- a/test/aaa_profiling/test_orm.py +++ b/test/aaa_profiling/test_orm.py @@ -109,7 +109,7 @@ class LoadManyToOneFromIdentityTest(_base.MappedTest): # so remove some platforms that have wildly divergent # callcounts. __requires__ = 'python25', - __unsupported_on__ = 'postgresql+pg8000', + __unsupported_on__ = 'postgresql+pg8000', 'mysql+pymysql' @classmethod def define_tables(cls, metadata): diff --git a/test/bootstrap/config.py b/test/bootstrap/config.py index fd43d0ca75..5bb301177d 100644 --- a/test/bootstrap/config.py +++ b/test/bootstrap/config.py @@ -23,6 +23,7 @@ pg8000=postgresql+pg8000://scott:tiger@127.0.0.1:5432/test postgresql_jython=postgresql+zxjdbc://scott:tiger@127.0.0.1:5432/test mysql_jython=mysql+zxjdbc://scott:tiger@127.0.0.1:5432/test mysql=mysql://scott:tiger@127.0.0.1:3306/test +pymysql=mysql+pymysql://scott:tiger@127.0.0.1:3306/test?use_unicode=0&charset=utf8 oracle=oracle://scott:tiger@127.0.0.1:1521 oracle8=oracle://scott:tiger@127.0.0.1:1521/?use_ansi=0 mssql=mssql://scott:tiger@SQUAWK\\SQLEXPRESS/test diff --git a/test/dialect/test_mysql.py b/test/dialect/test_mysql.py index 2fe9e75331..183f227c38 100644 --- a/test/dialect/test_mysql.py +++ b/test/dialect/test_mysql.py @@ -1404,6 +1404,7 @@ class MatchTest(TestBase, AssertsCompiledSQL): "MATCH (matchtable.title) AGAINST (%s IN BOOLEAN MODE)" % format) @testing.fails_on('mysql+mysqldb', 'uses format') + @testing.fails_on('mysql+pymysql', 'uses format') @testing.fails_on('mysql+oursql', 'uses format') @testing.fails_on('mysql+pyodbc', 'uses format') @testing.fails_on('mysql+zxjdbc', 'uses format') diff --git a/test/engine/test_execute.py b/test/engine/test_execute.py index 9379e207f6..318bc15d50 100644 --- a/test/engine/test_execute.py +++ b/test/engine/test_execute.py @@ -63,7 +63,7 @@ class ExecuteTest(TestBase): conn.execute('delete from users') # some psycopg2 versions bomb this. - @testing.fails_on_everything_except('mysql+mysqldb', + @testing.fails_on_everything_except('mysql+mysqldb', 'mysql+pymysql', 'mysql+mysqlconnector', 'postgresql') @testing.fails_on('postgresql+zxjdbc', 'sprintf not supported') def test_raw_sprintf(self): @@ -87,7 +87,8 @@ class ExecuteTest(TestBase): @testing.skip_if(lambda : testing.against('mysql+mysqldb'), 'db-api flaky') @testing.fails_on_everything_except('postgresql+psycopg2', - 'postgresql+pypostgresql', 'mysql+mysqlconnector') + 'postgresql+pypostgresql', 'mysql+mysqlconnector', + 'mysql+pymysql') def test_raw_python(self): for conn in testing.db, testing.db.connect(): conn.execute('insert into users (user_id, user_name) ' diff --git a/test/engine/test_reconnect.py b/test/engine/test_reconnect.py index ed68fa475f..31a2b705a9 100644 --- a/test/engine/test_reconnect.py +++ b/test/engine/test_reconnect.py @@ -403,6 +403,9 @@ class InvalidateDuringResultTest(TestBase): meta.drop_all() engine.dispose() + @testing.fails_on('+pymysql', + "Buffers the result set and doesn't check for " + "connection close") @testing.fails_on('+mysqldb', "Buffers the result set and doesn't check for " "connection close") diff --git a/test/lib/engines.py b/test/lib/engines.py index e6ea246e60..1acbdaf274 100644 --- a/test/lib/engines.py +++ b/test/lib/engines.py @@ -147,19 +147,15 @@ def utf8_engine(url=None, options=None): from sqlalchemy.engine import url as engine_url - if config.db.driver == 'mysqldb' and config.db.dialect.name != 'drizzle': - dbapi_ver = config.db.dialect.dbapi.version_info - if (dbapi_ver < (1, 2, 1) or - dbapi_ver in ((1, 2, 1, 'gamma', 1), (1, 2, 1, 'gamma', 2), - (1, 2, 1, 'gamma', 3), (1, 2, 1, 'gamma', 5))): - raise RuntimeError('Character set support unavailable with this ' - 'driver version: %s' % repr(dbapi_ver)) - else: - url = url or config.db_url - url = engine_url.make_url(url) - url.query['charset'] = 'utf8' - url.query['use_unicode'] = '0' - url = str(url) + if config.db.dialect.name == 'mysql' and \ + config.db.driver in ['mysqldb', 'pymysql']: + # note 1.2.1.gamma.6 or greater of MySQLdb + # needed here + url = url or config.db_url + url = engine_url.make_url(url) + url.query['charset'] = 'utf8' + url.query['use_unicode'] = '0' + url = str(url) return testing_engine(url, options) diff --git a/test/lib/requires.py b/test/lib/requires.py index 29c7d1ee4f..f26ba9c893 100644 --- a/test/lib/requires.py +++ b/test/lib/requires.py @@ -279,7 +279,8 @@ def cextensions(fn): def dbapi_lastrowid(fn): return _chain_decorators_on( fn, - fails_on_everything_except('mysql+mysqldb', 'mysql+oursql', 'sqlite+pysqlite') + fails_on_everything_except('mysql+mysqldb', 'mysql+oursql', + 'sqlite+pysqlite', 'mysql+pymysql') ) def sane_multi_rowcount(fn): diff --git a/test/sql/test_types.py b/test/sql/test_types.py index e41eab1407..52db03d769 100644 --- a/test/sql/test_types.py +++ b/test/sql/test_types.py @@ -477,7 +477,7 @@ class UnicodeTest(TestBase, AssertsExecutionResults): def test_round_trip(self): unicodedata = u"Alors vous imaginez ma surprise, au lever du jour, "\ - u"quand une drôle de petit voix m’a réveillé. Elle "\ + u"quand une drôle de petite voix m’a réveillé. Elle "\ u"disait: « S’il vous plaît… dessine-moi un mouton! »" unicode_table.insert().execute(unicode_varchar=unicodedata,unicode_text=unicodedata) @@ -493,7 +493,7 @@ class UnicodeTest(TestBase, AssertsExecutionResults): # vs. cursor.execute() unicodedata = u"Alors vous imaginez ma surprise, au lever du jour, quand "\ - u"une drôle de petit voix m’a réveillé. "\ + u"une drôle de petite voix m’a réveillé. "\ u"Elle disait: « S’il vous plaît… dessine-moi un mouton! »" unicode_table.insert().execute( @@ -511,7 +511,7 @@ class UnicodeTest(TestBase, AssertsExecutionResults): """ensure compiler processing works for UNIONs""" unicodedata = u"Alors vous imaginez ma surprise, au lever du jour, quand "\ - u"une drôle de petit voix m’a réveillé. "\ + u"une drôle de petite voix m’a réveillé. "\ u"Elle disait: « S’il vous plaît… dessine-moi un mouton! »" unicode_table.insert().execute(unicode_varchar=unicodedata,unicode_text=unicodedata) @@ -536,7 +536,7 @@ class UnicodeTest(TestBase, AssertsExecutionResults): """ unicodedata = u"Alors vous imaginez ma surprise, au lever du jour, quand "\ - u"une drôle de petit voix m’a réveillé. "\ + u"une drôle de petite voix m’a réveillé. "\ u"Elle disait: « S’il vous plaît… dessine-moi un mouton! »" # using Unicode explicly - warning should be emitted @@ -589,7 +589,7 @@ class UnicodeTest(TestBase, AssertsExecutionResults): """checks String(unicode_error='ignore') is passed to underlying codec.""" unicodedata = u"Alors vous imaginez ma surprise, au lever du jour, quand "\ - u"une drôle de petit voix m’a réveillé. "\ + u"une drôle de petite voix m’a réveillé. "\ u"Elle disait: « S’il vous plaît… dessine-moi un mouton! »" asciidata = unicodedata.encode('ascii', 'ignore') @@ -666,7 +666,7 @@ class UnicodeTest(TestBase, AssertsExecutionResults): eq_( x, u'Alors vous imaginez ma surprise, au lever du jour, quand une ' - u'drle de petit voix ma rveill. Elle disait: Sil vous plat ' + u'drle de petite voix ma rveill. Elle disait: Sil vous plat ' u'dessine-moi un mouton! ' ) elif engine.dialect.returns_unicode_strings: @@ -754,6 +754,7 @@ class EnumTest(TestBase): eq_(e1.adapt(ENUM).schema, 'bar') @testing.fails_on('mysql+mysqldb', "MySQL seems to issue a 'data truncated' warning.") + @testing.fails_on('mysql+pymysql', "MySQL seems to issue a 'data truncated' warning.") def test_constraint(self): assert_raises(exc.DBAPIError, enum_table.insert().execute,