New Upstream Release - python-mysqldb

Ready changes

Summary

Merged new upstream version: 2.1.1 (was: 1.4.6).

Resulting package

Built on 2023-01-25T01:19 (took 2m5s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases python3-mysqldb-dbgsymapt install -t fresh-releases python3-mysqldb

Lintian Result

Diff

diff --git a/HISTORY.rst b/HISTORY.rst
index 8c5399a..674c688 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -1,3 +1,77 @@
+======================
+ What's new in 2.1.1
+======================
+
+Release: 2022-06-22
+
+* Fix qualname of exception classes. (#522)
+* Fix range check in ``MySQLdb._mysql.result.fetch_row()``. Invalid ``how`` argument caused SEGV. (#538)
+* Fix docstring of ``_mysql.connect``. (#540)
+* Windows: Binary wheels are updated. (#541)
+   * Use MariaDB Connector/C 3.3.1.
+   * Use cibuildwheel to build wheels.
+   * Python 3.8-3.11
+
+======================
+ What's new in 2.1.0
+======================
+
+Release: 2021-11-17
+
+* Add ``multistatement=True`` option. You can disable multi statement. (#500).
+* Remove unnecessary bytes encoder which is remained for Django 1.11
+  compatibility (#490).
+* Deprecate ``passwd`` and ``db`` keyword. Use ``password`` and ``database``
+  instead. (#488).
+* Windows: Binary wheels are built with MariaDB Connector/C 3.2.4. (#508)
+* ``set_character_set()`` sends ``SET NAMES`` query always. This means
+  all new connections send it too. This solves compatibility issues
+  when server and client library are different version. (#509)
+* Remove ``escape()`` and ``escape_string()`` from ``MySQLdb`` package.
+  (#511)
+* Add Python 3.10 support and drop Python 3.5 support.
+
+======================
+ What's new in 2.0.3
+======================
+
+Release: 2021-01-01
+
+* Add ``-std=c99`` option to cflags by default for ancient compilers that doesn't
+  accept C99 by default.
+* You can customize cflags and ldflags by setting ``MYSQLCLIENT_CFLAGS`` and
+  ``MYSQLCLIENT_LDFLAGS``. It overrides ``mysql_config``.
+
+======================
+ What's new in 2.0.2
+======================
+
+Release: 2020-12-10
+
+* Windows: Update MariaDB Connector/C to 3.1.11.
+* Optimize fetching many rows with DictCursor.
+
+======================
+ What's new in 2.0.1
+======================
+
+Release: 2020-07-03
+
+* Fixed multithread safety issue in fetching row.
+* Removed obsolete members from Cursor. (e.g. `messages`, `_warnings`, `_last_executed`)
+
+======================
+ What's new in 2.0.0
+======================
+
+Release: 2020-07-02
+
+* Dropped Python 2 support
+* Dropped Django 1.11 support
+* Add context manager interface to Connection which closes the connection on ``__exit__``.
+* Add ``ssl_mode`` option.
+
+
 ======================
  What's new in 1.4.6
 ======================
@@ -34,7 +108,7 @@ Release: 2019-08-09
 
 * ``--static`` build supports ``libmariadbclient.a``
 * Try ``mariadb_config`` when ``mysql_config`` is not found
-* Fixed warning happend in Python 3.8 (#359)
+* Fixed warning happened in Python 3.8 (#359)
 * Fixed ``from MySQLdb import *``, while I don't recommend it. (#369)
 * Fixed SEGV ``MySQLdb.escape_string("1")`` when libmariadb is used and
   no connection is created. (#367)
@@ -234,7 +308,7 @@ More tests for date and time columns. (#41)
 
 Fix calling .execute() method for closed cursor cause TypeError. (#37)
 
-Improve peformance to parse date. (#43)
+Improve performance to parse date. (#43)
 
 Support geometry types (#49)
 
diff --git a/INSTALL.rst b/INSTALL.rst
deleted file mode 100644
index f5be3f4..0000000
--- a/INSTALL.rst
+++ /dev/null
@@ -1,146 +0,0 @@
-====================
-MySQLdb Installation
-====================
-
-.. contents::
-..
-
-Prerequisites
--------------
-
-+ Python 2.7, 3.5 or higher
-
-+ setuptools
-
-  * https://pypi.org/project/setuptools/
-
-+ MySQL 5.5 or higher
-
-  * https://www.mysql.com/downloads/
-
-  * MySQL 5.1 may work, but not supported.
-
-+ C compiler
-
-  * Most free software-based systems already have this, usually gcc.
-
-  * Most commercial UNIX platforms also come with a C compiler, or
-    you can also use gcc.
-
-  * If you have some Windows flavor, you should use Windows SDK or
-    Visual C++.
-
-
-Building and installing
------------------------
-
-The setup.py script uses mysql_config to find all compiler and linker
-options, and should work as is on any POSIX-like platform, so long as
-mysql_config is in your path.
-
-Depending on which version of MySQL you have, you may have the option
-of using three different client libraries. To select the client library,
-edit the [options] section of site.cfg:
-
-    static
-        if True, try to link against a static library; otherwise link
-        against dynamic libraries (default).
-        This option doesn't work for MySQL>5.6 since libmysqlclient
-        requires libstdc++. If you want to use, add `-lstdc++` to
-        mysql_config manually.
-
-If `<mysql prefix>/lib` is not added to `/etc/ld.so.conf`, `import _mysql`
-doesn't work. To fix this, (1) set `LD_LIBRARY_PATH`, or (2) add
-`-Wl,-rpath,<mysql prefix>/lib` to ldflags in your mysql_config.
-
-Finally, putting it together::
-
-  $ tar xz mysqlclient-1.3.6.tar.gz
-  $ cd mysqlclient-1.3.6
-  $ # edit site.cfg if necessary
-  $ python setup.py build
-  $ sudo python setup.py install # or su first
-
-
-Windows
-.......
-
-I don't do Windows. However if someone provides me with a package for
-Windows, I'll make it available. Don't ask me for help with Windows
-because I can't help you.
-
-Generally, though, running setup.py is similar to above::
-
-  C:\...> python setup.py install
-  C:\...> python setup.py bdist_wininst
-
-The latter example should build a Windows installer package, if you
-have the correct tools. In any event, you *must* have a C compiler.
-Additionally, you have to set an environment variable (mysqlroot)
-which is the path to your MySQL installation. In theory, it would be
-possible to get this information out of the registry, but like I said,
-I don't do Windows, but I'll accept a patch that does this.
-
-On Windows, you will definitely have to edit site.cfg since there is
-no mysql_config in the MySQL package.
-
-
-Binary Packages
----------------
-
-I don't plan to make binary packages any more. However, if someone
-contributes one, I will make it available. Several OS vendors have
-their own packages available.
-
-
-Red Hat Linux
-.............
-
-MySQL-python is pre-packaged in Red Hat Linux 7.x and newer. This
-includes Fedora Core and Red Hat Enterprise Linux.
-
-
-Debian GNU/Linux
-................
-
-Packaged as `python-mysqldb`_::
-
-    # apt-get install python-mysqldb
-
-Or use Synaptic.
-
-.. _`python-mysqldb`: http://packages.debian.org/python-mysqldb
-
-
-Ubuntu
-......
-
-Same as with Debian.
-
-
-Gentoo Linux
-............
-
-Packaged as `mysql-python`_. ::
-
-    # emerge sync
-    # emerge mysql-python
-    # emerge zmysqlda # if you use Zope
-
-.. _`mysql-python`: https://packages.gentoo.org/packages/search?q=mysql-python
-
-
-BSD
-...
-
-MySQL-python is a ported package in FreeBSD, NetBSD, and OpenBSD,
-although the name may vary to match OS conventions.
-
-
-License
--------
-
-GPL or the original license based on Python 1.5.2's license.
-
-
-:Author: Andy Dustman <andy@dustman.net>
diff --git a/MANIFEST.in b/MANIFEST.in
index 415a995..07563ca 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -3,7 +3,6 @@ recursive-include tests *.py
 include doc/conf.py
 include MANIFEST.in
 include HISTORY.rst
-include INSTALL.rst
 include README.md
 include LICENSE
 include metadata.cfg
diff --git a/MySQLdb/__init__.py b/MySQLdb/__init__.py
index cb1bd7c..b567363 100644
--- a/MySQLdb/__init__.py
+++ b/MySQLdb/__init__.py
@@ -13,28 +13,49 @@ For information on how MySQLdb handles type conversion, see the
 MySQLdb.converters module.
 """
 
-from MySQLdb.release import __version__, version_info, __author__
-
-from . import _mysql
-
-if version_info != _mysql.version_info:
-    raise ImportError("this is MySQLdb version %s, but _mysql is version %r\n_mysql: %r" %
-                      (version_info, _mysql.version_info, _mysql.__file__))
+try:
+    from MySQLdb.release import version_info
+    from . import _mysql
+
+    assert version_info == _mysql.version_info
+except Exception:
+    raise ImportError(
+        "this is MySQLdb version {}, but _mysql is version {!r}\n_mysql: {!r}".format(
+            version_info, _mysql.version_info, _mysql.__file__
+        )
+    )
+
+
+from ._mysql import (
+    NotSupportedError,
+    OperationalError,
+    get_client_info,
+    ProgrammingError,
+    Error,
+    InterfaceError,
+    debug,
+    IntegrityError,
+    string_literal,
+    MySQLError,
+    DataError,
+    DatabaseError,
+    InternalError,
+    Warning,
+)
+from MySQLdb.constants import FIELD_TYPE
+from MySQLdb.times import (
+    Date,
+    Time,
+    Timestamp,
+    DateFromTicks,
+    TimeFromTicks,
+    TimestampFromTicks,
+)
 
 threadsafety = 1
 apilevel = "2.0"
 paramstyle = "format"
 
-from ._mysql import *
-from MySQLdb.compat import PY2
-from MySQLdb.constants import FIELD_TYPE
-from MySQLdb.times import Date, Time, Timestamp, \
-    DateFromTicks, TimeFromTicks, TimestampFromTicks
-
-try:
-    frozenset
-except NameError:
-    from sets import ImmutableSet as frozenset
 
 class DBAPISet(frozenset):
     """A special type of set for which A == x is true if A is a
@@ -46,53 +67,104 @@ class DBAPISet(frozenset):
         return other in self
 
 
-STRING    = DBAPISet([FIELD_TYPE.ENUM, FIELD_TYPE.STRING,
-                     FIELD_TYPE.VAR_STRING])
-BINARY    = DBAPISet([FIELD_TYPE.BLOB, FIELD_TYPE.LONG_BLOB,
-                     FIELD_TYPE.MEDIUM_BLOB, FIELD_TYPE.TINY_BLOB])
-NUMBER    = DBAPISet([FIELD_TYPE.DECIMAL, FIELD_TYPE.DOUBLE, FIELD_TYPE.FLOAT,
-                     FIELD_TYPE.INT24, FIELD_TYPE.LONG, FIELD_TYPE.LONGLONG,
-                     FIELD_TYPE.TINY, FIELD_TYPE.YEAR, FIELD_TYPE.NEWDECIMAL])
-DATE      = DBAPISet([FIELD_TYPE.DATE])
-TIME      = DBAPISet([FIELD_TYPE.TIME])
+STRING = DBAPISet([FIELD_TYPE.ENUM, FIELD_TYPE.STRING, FIELD_TYPE.VAR_STRING])
+BINARY = DBAPISet(
+    [
+        FIELD_TYPE.BLOB,
+        FIELD_TYPE.LONG_BLOB,
+        FIELD_TYPE.MEDIUM_BLOB,
+        FIELD_TYPE.TINY_BLOB,
+    ]
+)
+NUMBER = DBAPISet(
+    [
+        FIELD_TYPE.DECIMAL,
+        FIELD_TYPE.DOUBLE,
+        FIELD_TYPE.FLOAT,
+        FIELD_TYPE.INT24,
+        FIELD_TYPE.LONG,
+        FIELD_TYPE.LONGLONG,
+        FIELD_TYPE.TINY,
+        FIELD_TYPE.YEAR,
+        FIELD_TYPE.NEWDECIMAL,
+    ]
+)
+DATE = DBAPISet([FIELD_TYPE.DATE])
+TIME = DBAPISet([FIELD_TYPE.TIME])
 TIMESTAMP = DBAPISet([FIELD_TYPE.TIMESTAMP, FIELD_TYPE.DATETIME])
-DATETIME  = TIMESTAMP
-ROWID     = DBAPISet()
+DATETIME = TIMESTAMP
+ROWID = DBAPISet()
+
 
 def test_DBAPISet_set_equality():
     assert STRING == STRING
 
+
 def test_DBAPISet_set_inequality():
     assert STRING != NUMBER
 
+
 def test_DBAPISet_set_equality_membership():
     assert FIELD_TYPE.VAR_STRING == STRING
 
+
 def test_DBAPISet_set_inequality_membership():
     assert FIELD_TYPE.DATE != STRING
 
-if PY2:
-    def Binary(x):
-        return bytearray(x)
-else:
-    def Binary(x):
-        return bytes(x)
+
+def Binary(x):
+    return bytes(x)
+
 
 def Connect(*args, **kwargs):
     """Factory function for connections.Connection."""
     from MySQLdb.connections import Connection
+
     return Connection(*args, **kwargs)
 
-connect = Connection = Connect
 
-__all__ = [ 'BINARY', 'Binary', 'Connect', 'Connection', 'DATE',
-    'Date', 'Time', 'Timestamp', 'DateFromTicks', 'TimeFromTicks',
-    'TimestampFromTicks', 'DataError', 'DatabaseError', 'Error',
-    'FIELD_TYPE', 'IntegrityError', 'InterfaceError', 'InternalError',
-    'MySQLError', 'NUMBER', 'NotSupportedError', 'DBAPISet',
-    'OperationalError', 'ProgrammingError', 'ROWID', 'STRING', 'TIME',
-    'TIMESTAMP', 'Warning', 'apilevel', 'connect', 'connections',
-    'constants', 'converters', 'cursors', 'debug', 'escape',
-    'escape_string', 'get_client_info',
-    'paramstyle', 'string_literal', 'threadsafety', 'version_info']
+connect = Connection = Connect
 
+__all__ = [
+    "BINARY",
+    "Binary",
+    "Connect",
+    "Connection",
+    "DATE",
+    "Date",
+    "Time",
+    "Timestamp",
+    "DateFromTicks",
+    "TimeFromTicks",
+    "TimestampFromTicks",
+    "DataError",
+    "DatabaseError",
+    "Error",
+    "FIELD_TYPE",
+    "IntegrityError",
+    "InterfaceError",
+    "InternalError",
+    "MySQLError",
+    "NUMBER",
+    "NotSupportedError",
+    "DBAPISet",
+    "OperationalError",
+    "ProgrammingError",
+    "ROWID",
+    "STRING",
+    "TIME",
+    "TIMESTAMP",
+    "Warning",
+    "apilevel",
+    "connect",
+    "connections",
+    "constants",
+    "converters",
+    "cursors",
+    "debug",
+    "get_client_info",
+    "paramstyle",
+    "string_literal",
+    "threadsafety",
+    "version_info",
+]
diff --git a/MySQLdb/_exceptions.py b/MySQLdb/_exceptions.py
index 0f14f3b..a5aca7e 100644
--- a/MySQLdb/_exceptions.py
+++ b/MySQLdb/_exceptions.py
@@ -4,38 +4,49 @@ These classes are dictated by the DB API v2.0:
 
     https://www.python.org/dev/peps/pep-0249/
 """
-from .compat import StandardError
 
 
-class MySQLError(StandardError):
+class MySQLError(Exception):
     """Exception related to operation with MySQL."""
 
+    __module__ = "MySQLdb"
+
 
 class Warning(Warning, MySQLError):
     """Exception raised for important warnings like data truncations
     while inserting, etc."""
 
+    __module__ = "MySQLdb"
+
 
 class Error(MySQLError):
     """Exception that is the base class of all other error exceptions
     (not Warning)."""
 
+    __module__ = "MySQLdb"
+
 
 class InterfaceError(Error):
     """Exception raised for errors that are related to the database
     interface rather than the database itself."""
 
+    __module__ = "MySQLdb"
+
 
 class DatabaseError(Error):
     """Exception raised for errors that are related to the
     database."""
 
+    __module__ = "MySQLdb"
+
 
 class DataError(DatabaseError):
     """Exception raised for errors that are due to problems with the
     processed data like division by zero, numeric value out of range,
     etc."""
 
+    __module__ = "MySQLdb"
+
 
 class OperationalError(DatabaseError):
     """Exception raised for errors that are related to the database's
@@ -44,27 +55,37 @@ class OperationalError(DatabaseError):
     found, a transaction could not be processed, a memory allocation
     error occurred during processing, etc."""
 
+    __module__ = "MySQLdb"
+
 
 class IntegrityError(DatabaseError):
     """Exception raised when the relational integrity of the database
     is affected, e.g. a foreign key check fails, duplicate key,
     etc."""
 
+    __module__ = "MySQLdb"
+
 
 class InternalError(DatabaseError):
     """Exception raised when the database encounters an internal
     error, e.g. the cursor is not valid anymore, the transaction is
     out of sync, etc."""
 
+    __module__ = "MySQLdb"
+
 
 class ProgrammingError(DatabaseError):
     """Exception raised for programming errors, e.g. table not found
     or already exists, syntax error in the SQL statement, wrong number
     of parameters specified, etc."""
 
+    __module__ = "MySQLdb"
+
 
 class NotSupportedError(DatabaseError):
     """Exception raised in case a method or database API was used
     which is not supported by the database, e.g. requesting a
     .rollback() on a connection that does not support transaction or
     has transactions turned off."""
+
+    __module__ = "MySQLdb"
diff --git a/MySQLdb/_mysql.c b/MySQLdb/_mysql.c
index 64647bf..7737dbe 100644
--- a/MySQLdb/_mysql.c
+++ b/MySQLdb/_mysql.c
@@ -34,16 +34,19 @@ PERFORMANCE OF THIS SOFTWARE.
 #define my_bool _Bool
 #endif
 
+#if ((MYSQL_VERSION_ID >= 50555 && MYSQL_VERSION_ID <= 50599) || \
+(MYSQL_VERSION_ID >= 50636 && MYSQL_VERSION_ID <= 50699) || \
+(MYSQL_VERSION_ID >= 50711 && MYSQL_VERSION_ID <= 50799) || \
+(MYSQL_VERSION_ID >= 80000)) && \
+!defined(MARIADB_BASE_VERSION) && !defined(MARIADB_VERSION_ID)
+#define HAVE_ENUM_MYSQL_OPT_SSL_MODE
+#endif
+
 #define PY_SSIZE_T_CLEAN 1
 #include "Python.h"
-#if PY_MAJOR_VERSION >= 3
-#define IS_PY3K
-#define PyInt_Type PyLong_Type
-#define PyInt_FromString PyLong_FromString
-#define PyInt_FromLong(n) PyLong_FromLong(n)
-#define PyInt_Check(n) PyLong_Check(n)
-#define PyInt_AS_LONG(n) PyLong_AS_LONG(n)
-#define PyString_FromString(s) PyUnicode_FromString(s)
+
+#if PY_MAJOR_VERSION == 2
+#error "Python 2 is not supported"
 #endif
 
 #include "bytesobject.h"
@@ -200,8 +203,8 @@ _mysql_Exception(_mysql_ConnectionObject *c)
             e = _mysql_OperationalError;
         break;
     }
-    PyTuple_SET_ITEM(t, 0, PyInt_FromLong((long)merr));
-    PyTuple_SET_ITEM(t, 1, PyString_FromString(mysql_error(&(c->connection))));
+    PyTuple_SET_ITEM(t, 0, PyLong_FromLong((long)merr));
+    PyTuple_SET_ITEM(t, 1, PyUnicode_FromString(mysql_error(&(c->connection))));
     PyErr_SetObject(e, t);
     Py_DECREF(t);
     return NULL;
@@ -286,7 +289,7 @@ _mysql_ResultObject_Initialize(
     fields = mysql_fetch_fields(result);
     for (i=0; i<n; i++) {
         PyObject *tmp, *fun;
-        tmp = PyInt_FromLong((long) fields[i].type);
+        tmp = PyLong_FromLong((long) fields[i].type);
         if (!tmp) {
             return -1;
         }
@@ -307,7 +310,7 @@ _mysql_ResultObject_Initialize(
             PyObject *fun2=NULL;
             int j, n2=PySequence_Size(fun);
             // BINARY_FLAG means ***_bin collation is used.
-            // To distinguish text and binary, we shoud use charsetnr==63 (binary).
+            // To distinguish text and binary, we should use charsetnr==63 (binary).
             // But we abuse BINARY_FLAG for historical reason.
             if (fields[i].charsetnr == 63) {
                 flags |= BINARY_FLAG;
@@ -326,8 +329,8 @@ _mysql_ResultObject_Initialize(
                     pmask = PyTuple_GET_ITEM(t, 0);
                     fun2 = PyTuple_GET_ITEM(t, 1);
                     Py_XINCREF(fun2);
-                    if (PyInt_Check(pmask)) {
-                        mask = PyInt_AS_LONG(pmask);
+                    if (PyLong_Check(pmask)) {
+                        mask = PyLong_AS_LONG(pmask);
                         if (mask & flags) {
                             Py_DECREF(t);
                             break;
@@ -376,6 +379,23 @@ static int _mysql_ResultObject_clear(_mysql_ResultObject *self)
     return 0;
 }
 
+#ifdef HAVE_ENUM_MYSQL_OPT_SSL_MODE
+static int
+_get_ssl_mode_num(char *ssl_mode)
+{
+    static char *ssl_mode_list[] = { "DISABLED", "PREFERRED",
+                  "REQUIRED", "VERIFY_CA", "VERIFY_IDENTITY" };
+    unsigned int i;
+    for (i=0; i < sizeof(ssl_mode_list)/sizeof(ssl_mode_list[0]); i++) {
+        if (strcmp(ssl_mode, ssl_mode_list[i]) == 0) {
+            // SSL_MODE one-based
+            return i + 1;
+        }
+    }
+    return -1;
+}
+#endif
+
 static int
 _mysql_ConnectionObject_Initialize(
     _mysql_ConnectionObject *self,
@@ -385,6 +405,7 @@ _mysql_ConnectionObject_Initialize(
     MYSQL *conn = NULL;
     PyObject *conv = NULL;
     PyObject *ssl = NULL;
+    char *ssl_mode = NULL;
     const char *key = NULL, *cert = NULL, *ca = NULL,
          *capath = NULL, *cipher = NULL;
     PyObject *ssl_keepref[5] = {NULL};
@@ -393,12 +414,12 @@ _mysql_ConnectionObject_Initialize(
          *db = NULL, *unix_socket = NULL;
     unsigned int port = 0;
     unsigned int client_flag = 0;
-    static char *kwlist[] = { "host", "user", "passwd", "db", "port",
+    static char *kwlist[] = { "host", "user", "password", "database", "port",
                   "unix_socket", "conv",
                   "connect_timeout", "compress",
                   "named_pipe", "init_command",
                   "read_default_file", "read_default_group",
-                  "client_flag", "ssl",
+                  "client_flag", "ssl", "ssl_mode",
                   "local_infile",
                   "read_timeout", "write_timeout", "charset",
                   "auth_plugin",
@@ -417,7 +438,7 @@ _mysql_ConnectionObject_Initialize(
     self->open = 0;
 
     if (!PyArg_ParseTupleAndKeywords(args, kwargs,
-                "|ssssisOiiisssiOiiiss:connect",
+                "|ssssisOiiisssiOsiiiss:connect",
                 kwlist,
                 &host, &user, &passwd, &db,
                 &port, &unix_socket, &conv,
@@ -425,7 +446,7 @@ _mysql_ConnectionObject_Initialize(
                 &compress, &named_pipe,
                 &init_command, &read_default_file,
                 &read_default_group,
-                &client_flag, &ssl,
+                &client_flag, &ssl, &ssl_mode,
                 &local_infile,
                 &read_timeout,
                 &write_timeout,
@@ -434,15 +455,9 @@ _mysql_ConnectionObject_Initialize(
     ))
         return -1;
 
-#ifdef IS_PY3K
 #define _stringsuck(d,t,s) {t=PyMapping_GetItemString(s,#d);\
         if(t){d=PyUnicode_AsUTF8(t);ssl_keepref[n_ssl_keepref++]=t;}\
         PyErr_Clear();}
-#else
-#define _stringsuck(d,t,s) {t=PyMapping_GetItemString(s,#d);\
-        if(t){d=PyString_AsString(t);ssl_keepref[n_ssl_keepref++]=t;}\
-        PyErr_Clear();}
-#endif
 
     if (ssl) {
         PyObject *value = NULL;
@@ -452,6 +467,17 @@ _mysql_ConnectionObject_Initialize(
         _stringsuck(key, value, ssl);
         _stringsuck(cipher, value, ssl);
     }
+    if (ssl_mode) {
+#ifdef HAVE_ENUM_MYSQL_OPT_SSL_MODE
+        if (_get_ssl_mode_num(ssl_mode) <= 0) {
+            PyErr_SetString(_mysql_NotSupportedError, "Unknown ssl_mode specification");
+            return -1;
+        }
+#else
+        PyErr_SetString(_mysql_NotSupportedError, "MySQL client library does not support ssl_mode specification");
+        return -1;
+#endif
+    }
 
     conn = mysql_init(&(self->connection));
     if (!conn) {
@@ -494,6 +520,12 @@ _mysql_ConnectionObject_Initialize(
     if (ssl) {
         mysql_ssl_set(&(self->connection), key, cert, ca, capath, cipher);
     }
+#ifdef HAVE_ENUM_MYSQL_OPT_SSL_MODE
+    if (ssl_mode) {
+        int ssl_mode_num = _get_ssl_mode_num(ssl_mode);
+        mysql_options(&(self->connection), MYSQL_OPT_SSL_MODE, &ssl_mode_num);
+    }
+#endif
     if (charset) {
         mysql_options(&(self->connection), MYSQL_SET_CHARSET_NAME, charset);
     }
@@ -549,10 +581,10 @@ host\n\
 user\n\
   string, user to connect as\n\
 \n\
-passwd\n\
+password\n\
   string, password to use\n\
 \n\
-db\n\
+database\n\
   string, database to use\n\
 \n\
 port\n\
@@ -658,7 +690,7 @@ _mysql_ConnectionObject_affected_rows(
     check_connection(self);
     ret = mysql_affected_rows(&(self->connection));
     if (ret == (my_ulonglong)-1)
-        return PyInt_FromLong(-1);
+        return PyLong_FromLong(-1);
     return PyLong_FromUnsignedLongLong(ret);
 }
 
@@ -790,7 +822,7 @@ _mysql_ConnectionObject_next_result(
     err = mysql_next_result(&(self->connection));
     Py_END_ALLOW_THREADS
     if (err > 0) return _mysql_Exception(self);
-    return PyInt_FromLong(err);
+    return PyLong_FromLong(err);
 }
 
 
@@ -813,7 +845,7 @@ _mysql_ConnectionObject_set_server_option(
     err = mysql_set_server_option(&(self->connection), flags);
     Py_END_ALLOW_THREADS
     if (err) return _mysql_Exception(self);
-    return PyInt_FromLong(err);
+    return PyLong_FromLong(err);
 }
 
 static char _mysql_ConnectionObject_sqlstate__doc__[] =
@@ -834,7 +866,7 @@ _mysql_ConnectionObject_sqlstate(
     PyObject *noargs)
 {
     check_connection(self);
-    return PyString_FromString(mysql_sqlstate(&(self->connection)));
+    return PyUnicode_FromString(mysql_sqlstate(&(self->connection)));
 }
 
 static char _mysql_ConnectionObject_warning_count__doc__[] =
@@ -849,7 +881,7 @@ _mysql_ConnectionObject_warning_count(
     PyObject *noargs)
 {
     check_connection(self);
-    return PyInt_FromLong(mysql_warning_count(&(self->connection)));
+    return PyLong_FromLong(mysql_warning_count(&(self->connection)));
 }
 
 static char _mysql_ConnectionObject_errno__doc__[] =
@@ -864,7 +896,7 @@ _mysql_ConnectionObject_errno(
     PyObject *noargs)
 {
     check_connection(self);
-    return PyInt_FromLong((long)mysql_errno(&(self->connection)));
+    return PyLong_FromLong((long)mysql_errno(&(self->connection)));
 }
 
 static char _mysql_ConnectionObject_error__doc__[] =
@@ -879,7 +911,7 @@ _mysql_ConnectionObject_error(
     PyObject *noargs)
 {
     check_connection(self);
-    return PyString_FromString(mysql_error(&(self->connection)));
+    return PyUnicode_FromString(mysql_error(&(self->connection)));
 }
 
 static char _mysql_escape_string__doc__[] =
@@ -948,14 +980,12 @@ _mysql_string_literal(
     } else {
         s = PyObject_Str(o);
         if (!s) return NULL;
-#ifdef IS_PY3K
         {
             PyObject *t = PyUnicode_AsASCIIString(s);
             Py_DECREF(s);
             if (!t) return NULL;
             s = t;
         }
-#endif
     }
     in = PyBytes_AsString(s);
     size = PyBytes_GET_SIZE(s);
@@ -1055,7 +1085,6 @@ _mysql_ResultObject_describe(
     if (!(d = PyTuple_New(n))) return NULL;
     for (i=0; i<n; i++) {
         PyObject *t;
-#ifdef IS_PY3K
         PyObject *name;
         if (self->encoding == utf8) {
             name = PyUnicode_DecodeUTF8(fields[i].name, fields[i].name_length, "replace");
@@ -1068,10 +1097,6 @@ _mysql_ResultObject_describe(
 
         t = Py_BuildValue("(Niiiiii)",
                   name,
-#else
-        t = Py_BuildValue("(siiiiii)",
-                  fields[i].name,
-#endif
                   (long) fields[i].type,
                   (long) fields[i].max_length,
                   (long) fields[i].length,
@@ -1105,7 +1130,7 @@ _mysql_ResultObject_field_flags(
     if (!(d = PyTuple_New(n))) return NULL;
     for (i=0; i<n; i++) {
         PyObject *f;
-        if (!(f = PyInt_FromLong((long)fields[i].flags))) goto error;
+        if (!(f = PyLong_FromLong((long)fields[i].flags))) goto error;
         PyTuple_SET_ITEM(d, i, f);
     }
     return d;
@@ -1140,50 +1165,37 @@ _mysql_field_to_python(
         //fprintf(stderr, "decoding with bytes\n", encoding);
         return PyBytes_FromStringAndSize(rowitem, length);
     }
-    if (converter == (PyObject*)&PyInt_Type) {
+    if (converter == (PyObject*)&PyLong_Type) {
         //fprintf(stderr, "decoding with int\n", encoding);
-        return PyInt_FromString(rowitem, NULL, 10);
+        return PyLong_FromString(rowitem, NULL, 10);
     }
 
     //fprintf(stderr, "decoding with callback\n");
     //PyObject_Print(converter, stderr, 0);
     //fprintf(stderr, "\n");
-#ifdef IS_PY3K
     int binary;
     switch (field->type) {
-    case FIELD_TYPE_TINY_BLOB:
-    case FIELD_TYPE_MEDIUM_BLOB:
-    case FIELD_TYPE_LONG_BLOB:
-    case FIELD_TYPE_BLOB:
-    case FIELD_TYPE_VAR_STRING:
-    case FIELD_TYPE_STRING:
-    case FIELD_TYPE_GEOMETRY:
-    case FIELD_TYPE_BIT:
-#ifdef FIELD_TYPE_JSON
-    case FIELD_TYPE_JSON:
-#else
-    case 245: // JSON
-#endif
-        // Call converter with bytes
-        binary = 1;
+    case FIELD_TYPE_DECIMAL:
+    case FIELD_TYPE_NEWDECIMAL:
+    case FIELD_TYPE_TIMESTAMP:
+    case FIELD_TYPE_DATETIME:
+    case FIELD_TYPE_TIME:
+    case FIELD_TYPE_DATE:
+        binary = 0;  // pass str, because these converters expect it
         break;
-    default: // e.g. FIELD_TYPE_DATETIME, etc.
-        // Call converter with unicode string
-        binary = 0;
+    default: // Default to just passing bytes
+        binary = 1;
     }
     return PyObject_CallFunction(converter,
             binary ? "y#" : "s#",
             rowitem, (Py_ssize_t)length);
-#else
-    return PyObject_CallFunction(converter,
-            "s#", rowitem, (Py_ssize_t)length);
-#endif
 }
 
 static PyObject *
 _mysql_row_to_tuple(
     _mysql_ResultObject *self,
-    MYSQL_ROW row)
+    MYSQL_ROW row,
+    PyObject *unused)
 {
     unsigned int n, i;
     unsigned long *length;
@@ -1210,7 +1222,8 @@ _mysql_row_to_tuple(
 static PyObject *
 _mysql_row_to_dict(
     _mysql_ResultObject *self,
-    MYSQL_ROW row)
+    MYSQL_ROW row,
+    PyObject *cache)
 {
     unsigned int n, i;
     unsigned long *length;
@@ -1226,30 +1239,48 @@ _mysql_row_to_dict(
         c = PyTuple_GET_ITEM(self->converter, i);
         v = _mysql_field_to_python(c, row[i], length[i], &fields[i], self->encoding);
         if (!v) goto error;
-        if (!PyMapping_HasKeyString(r, fields[i].name)) {
-            PyMapping_SetItemString(r, fields[i].name, v);
+
+        PyObject *pyname = PyUnicode_FromString(fields[i].name);
+        if (pyname == NULL) {
+            Py_DECREF(v);
+            goto error;
+        }
+        int err = PyDict_Contains(r, pyname);
+        if (err < 0) { // error
+            Py_DECREF(v);
+            goto error;
+        }
+        if (err) { // duplicate
+            Py_DECREF(pyname);
+            pyname = PyUnicode_FromFormat("%s.%s", fields[i].table, fields[i].name);
+            if (pyname == NULL) {
+                Py_DECREF(v);
+                goto error;
+            }
+        }
+
+        err = PyDict_SetItem(r, pyname, v);
+        if (cache) {
+            PyTuple_SET_ITEM(cache, i, pyname);
         } else {
-            int len;
-            char buf[256];
-            strncpy(buf, fields[i].table, 256);
-            len = strlen(buf);
-            strncat(buf, ".", 256-len);
-            len = strlen(buf);
-            strncat(buf, fields[i].name, 256-len);
-            PyMapping_SetItemString(r, buf, v);
+            Py_DECREF(pyname);
         }
         Py_DECREF(v);
+        if (err) {
+            goto error;
+        }
     }
     return r;
-  error:
-    Py_XDECREF(r);
+error:
+    Py_DECREF(r);
     return NULL;
 }
 
 static PyObject *
 _mysql_row_to_dict_old(
     _mysql_ResultObject *self,
-    MYSQL_ROW row)
+    MYSQL_ROW row,
+    PyObject *cache)
 {
     unsigned int n, i;
     unsigned long *length;
@@ -1264,20 +1295,61 @@ _mysql_row_to_dict_old(
         PyObject *v;
         c = PyTuple_GET_ITEM(self->converter, i);
         v = _mysql_field_to_python(c, row[i], length[i], &fields[i], self->encoding);
-        if (!v) goto error;
-        {
-            int len=0;
-            char buf[256]="";
-            if (strlen(fields[i].table)) {
-                strncpy(buf, fields[i].table, 256);
-                len = strlen(buf);
-                strncat(buf, ".", 256-len);
-                len = strlen(buf);
-            }
-            strncat(buf, fields[i].name, 256-len);
-            PyMapping_SetItemString(r, buf, v);
+        if (!v) {
+            goto error;
+        }
+
+        PyObject *pyname;
+        if (strlen(fields[i].table)) {
+            pyname = PyUnicode_FromFormat("%s.%s", fields[i].table, fields[i].name);
+        } else {
+            pyname = PyUnicode_FromString(fields[i].name);
+        }
+        int err = PyDict_SetItem(r, pyname, v);
+        Py_DECREF(v);
+        if (cache) {
+            PyTuple_SET_ITEM(cache, i, pyname);
+        } else {
+            Py_DECREF(pyname);
+        }
+        if (err) {
+            goto error;
         }
+    }
+    return r;
+  error:
+    Py_XDECREF(r);
+    return NULL;
+}
+
+static PyObject *
+_mysql_row_to_dict_cached(
+    _mysql_ResultObject *self,
+    MYSQL_ROW row,
+    PyObject *cache)
+{
+    PyObject *r = PyDict_New();
+    if (!r) {
+        return NULL;
+    }
+
+    unsigned int n = mysql_num_fields(self->result);
+    unsigned long *length = mysql_fetch_lengths(self->result);
+    MYSQL_FIELD *fields = mysql_fetch_fields(self->result);
+
+    for (unsigned int i=0; i<n; i++) {
+        PyObject *c = PyTuple_GET_ITEM(self->converter, i);
+        PyObject *v = _mysql_field_to_python(c, row[i], length[i], &fields[i], self->encoding);
+        if (!v) {
+            goto error;
+        }
+
+        PyObject *pyname = PyTuple_GET_ITEM(cache, i); // borrowed
+        int err = PyDict_SetItem(r, pyname, v);
         Py_DECREF(v);
+        if (err) {
+            goto error;
+        }
     }
     return r;
   error:
@@ -1285,21 +1357,34 @@ _mysql_row_to_dict_old(
     return NULL;
 }
 
-typedef PyObject *_PYFUNC(_mysql_ResultObject *, MYSQL_ROW);
 
-int
+typedef PyObject *_convertfunc(_mysql_ResultObject *, MYSQL_ROW, PyObject *);
+static _convertfunc * const row_converters[] = {
+    _mysql_row_to_tuple,
+    _mysql_row_to_dict,
+    _mysql_row_to_dict_old
+};
+
+Py_ssize_t
 _mysql__fetch_row(
     _mysql_ResultObject *self,
-    PyObject **r,
-    int skiprows,
-    int maxrows,
-    _PYFUNC *convert_row)
+    PyObject *r, /* list object */
+    Py_ssize_t maxrows,
+    int how)
 {
-    int i;
-    MYSQL_ROW row;
+    _convertfunc *convert_row = row_converters[how];
 
-    for (i = skiprows; i<(skiprows+maxrows); i++) {
-        PyObject *v;
+    PyObject *cache = NULL;
+    if (maxrows > 0 && how > 0) {
+        cache = PyTuple_New(mysql_num_fields(self->result));
+        if (!cache) {
+            return -1;
+        }
+    }
+
+    Py_ssize_t i;
+    for (i = 0; i < maxrows; i++) {
+        MYSQL_ROW row;
         if (!self->use)
             row = mysql_fetch_row(self->result);
         else {
@@ -1312,15 +1397,25 @@ _mysql__fetch_row(
             goto error;
         }
         if (!row) {
-            if (_PyTuple_Resize(r, i) == -1) goto error;
             break;
         }
-        v = convert_row(self, row);
-        if (!v) goto error;
-        PyTuple_SET_ITEM(*r, i, v);
+        PyObject *v = convert_row(self, row, cache);
+        if (!v) {
+            goto error;
+        }
+        if (cache) {
+            convert_row = _mysql_row_to_dict_cached;
+        }
+        if (PyList_Append(r, v)) {
+            Py_DECREF(v);
+            goto error;
+        }
+        Py_DECREF(v);
     }
-    return i-skiprows;
-  error:
+    Py_XDECREF(cache);
+    return i;
+error:
+    Py_XDECREF(cache);
     return -1;
 }
 
@@ -1339,55 +1434,36 @@ _mysql_ResultObject_fetch_row(
     PyObject *args,
     PyObject *kwargs)
 {
-    typedef PyObject *_PYFUNC(_mysql_ResultObject *, MYSQL_ROW);
-    static char *kwlist[] = { "maxrows", "how", NULL };
-    static _PYFUNC *row_converters[] =
-    {
-        _mysql_row_to_tuple,
-        _mysql_row_to_dict,
-        _mysql_row_to_dict_old
-    };
-    _PYFUNC *convert_row;
-    int maxrows=1, how=0, skiprows=0, rowsadded;
+    static char *kwlist[] = {"maxrows", "how", NULL };
+    int maxrows=1, how=0;
     PyObject *r=NULL;
 
     if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii:fetch_row", kwlist,
                      &maxrows, &how))
         return NULL;
     check_result_connection(self);
-    if (how >= (int)sizeof(row_converters)) {
+    if (how >= (int)(sizeof(row_converters) / sizeof(row_converters[0]))) {
         PyErr_SetString(PyExc_ValueError, "how out of range");
         return NULL;
     }
-    convert_row = row_converters[how];
-    if (maxrows) {
-        if (!(r = PyTuple_New(maxrows))) goto error;
-        rowsadded = _mysql__fetch_row(self, &r, skiprows, maxrows,
-                convert_row);
-        if (rowsadded == -1) goto error;
-    } else {
+    if (!maxrows) {
         if (self->use) {
-            maxrows = 1000;
-            if (!(r = PyTuple_New(maxrows))) goto error;
-            while (1) {
-                rowsadded = _mysql__fetch_row(self, &r, skiprows,
-                        maxrows, convert_row);
-                if (rowsadded == -1) goto error;
-                skiprows += rowsadded;
-                if (rowsadded < maxrows) break;
-                if (_PyTuple_Resize(&r, skiprows+maxrows) == -1)
-                        goto error;
-            }
+            maxrows = INT_MAX;
         } else {
-            /* XXX if overflow, maxrows<0? */
-            maxrows = (int) mysql_num_rows(self->result);
-            if (!(r = PyTuple_New(maxrows))) goto error;
-            rowsadded = _mysql__fetch_row(self, &r, 0,
-                    maxrows, convert_row);
-            if (rowsadded == -1) goto error;
+            // todo: preallocate.
+            maxrows = (Py_ssize_t) mysql_num_rows(self->result);
         }
     }
-    return r;
+    if (!(r = PyList_New(0))) goto error;
+    Py_ssize_t rowsadded = _mysql__fetch_row(self, r, maxrows, how);
+    if (rowsadded == -1) goto error;
+
+    /* DB-API allows return rows as list.
+     * But we need to return list because Django expecting tuple.
+     */
+    PyObject *t = PyList_AsTuple(r);
+    Py_DECREF(r);
+    return t;
   error:
     Py_XDECREF(r);
     return NULL;
@@ -1444,7 +1520,7 @@ _mysql_ConnectionObject_character_set_name(
     const char *s;
     check_connection(self);
     s = mysql_character_set_name(&(self->connection));
-    return PyString_FromString(s);
+    return PyUnicode_FromString(s);
 }
 
 static char _mysql_ConnectionObject_set_character_set__doc__[] =
@@ -1502,15 +1578,15 @@ _mysql_ConnectionObject_get_character_set_info(
     mysql_get_character_set_info(&(self->connection), &cs);
     if (!(result = PyDict_New())) return NULL;
     if (cs.csname)
-        PyDict_SetItemString(result, "name", PyString_FromString(cs.csname));
+        PyDict_SetItemString(result, "name", PyUnicode_FromString(cs.csname));
     if (cs.name)
-        PyDict_SetItemString(result, "collation", PyString_FromString(cs.name));
+        PyDict_SetItemString(result, "collation", PyUnicode_FromString(cs.name));
     if (cs.comment)
-        PyDict_SetItemString(result, "comment", PyString_FromString(cs.comment));
+        PyDict_SetItemString(result, "comment", PyUnicode_FromString(cs.comment));
     if (cs.dir)
-        PyDict_SetItemString(result, "dir", PyString_FromString(cs.dir));
-    PyDict_SetItemString(result, "mbminlen", PyInt_FromLong(cs.mbminlen));
-    PyDict_SetItemString(result, "mbmaxlen", PyInt_FromLong(cs.mbmaxlen));
+        PyDict_SetItemString(result, "dir", PyUnicode_FromString(cs.dir));
+    PyDict_SetItemString(result, "mbminlen", PyLong_FromLong(cs.mbminlen));
+    PyDict_SetItemString(result, "mbmaxlen", PyLong_FromLong(cs.mbmaxlen));
     return result;
 }
 #endif
@@ -1545,7 +1621,7 @@ _mysql_get_client_info(
     PyObject *self,
     PyObject *noargs)
 {
-    return PyString_FromString(mysql_get_client_info());
+    return PyUnicode_FromString(mysql_get_client_info());
 }
 
 static char _mysql_ConnectionObject_get_host_info__doc__[] =
@@ -1559,7 +1635,7 @@ _mysql_ConnectionObject_get_host_info(
     PyObject *noargs)
 {
     check_connection(self);
-    return PyString_FromString(mysql_get_host_info(&(self->connection)));
+    return PyUnicode_FromString(mysql_get_host_info(&(self->connection)));
 }
 
 static char _mysql_ConnectionObject_get_proto_info__doc__[] =
@@ -1573,7 +1649,7 @@ _mysql_ConnectionObject_get_proto_info(
     PyObject *noargs)
 {
     check_connection(self);
-    return PyInt_FromLong((long)mysql_get_proto_info(&(self->connection)));
+    return PyLong_FromLong((long)mysql_get_proto_info(&(self->connection)));
 }
 
 static char _mysql_ConnectionObject_get_server_info__doc__[] =
@@ -1587,7 +1663,7 @@ _mysql_ConnectionObject_get_server_info(
     PyObject *noargs)
 {
     check_connection(self);
-    return PyString_FromString(mysql_get_server_info(&(self->connection)));
+    return PyUnicode_FromString(mysql_get_server_info(&(self->connection)));
 }
 
 static char _mysql_ConnectionObject_info__doc__[] =
@@ -1604,7 +1680,7 @@ _mysql_ConnectionObject_info(
     const char *s;
     check_connection(self);
     s = mysql_info(&(self->connection));
-    if (s) return PyString_FromString(s);
+    if (s) return PyUnicode_FromString(s);
     Py_RETURN_NONE;
 }
 
@@ -1674,7 +1750,7 @@ _mysql_ConnectionObject_field_count(
     PyObject *noargs)
 {
     check_connection(self);
-    return PyInt_FromLong((long)mysql_field_count(&(self->connection)));
+    return PyLong_FromLong((long)mysql_field_count(&(self->connection)));
 }
 
 static char _mysql_ConnectionObject_fileno__doc__[] =
@@ -1688,7 +1764,7 @@ _mysql_ConnectionObject_fileno(
     PyObject *noargs)
 {
     check_connection(self);
-    return PyInt_FromLong(self->connection.net.fd);
+    return PyLong_FromLong(self->connection.net.fd);
 }
 
 static char _mysql_ResultObject_num_fields__doc__[] =
@@ -1700,7 +1776,7 @@ _mysql_ResultObject_num_fields(
     PyObject *noargs)
 {
     check_result_connection(self);
-    return PyInt_FromLong((long)mysql_num_fields(self->result));
+    return PyLong_FromLong((long)mysql_num_fields(self->result));
 }
 
 static char _mysql_ResultObject_num_rows__doc__[] =
@@ -1889,7 +1965,7 @@ _mysql_ConnectionObject_stat(
     s = mysql_stat(&(self->connection));
     Py_END_ALLOW_THREADS
     if (!s) return _mysql_Exception(self);
-    return PyString_FromString(s);
+    return PyUnicode_FromString(s);
 }
 
 static char _mysql_ConnectionObject_store_result__doc__[] =
@@ -1950,7 +2026,7 @@ _mysql_ConnectionObject_thread_id(
     Py_BEGIN_ALLOW_THREADS
     pid = mysql_thread_id(&(self->connection));
     Py_END_ALLOW_THREADS
-    return PyInt_FromLong((long)pid);
+    return PyLong_FromLong((long)pid);
 }
 
 static char _mysql_ConnectionObject_use_result__doc__[] =
@@ -2013,7 +2089,7 @@ _mysql_ConnectionObject_repr(
             self->connection.host, self);
     else
         snprintf(buf, 300, "<_mysql.connection closed at %p>", self);
-    return PyString_FromString(buf);
+    return PyUnicode_FromString(buf);
 }
 
 static char _mysql_ResultObject_data_seek__doc__[] =
@@ -2046,7 +2122,7 @@ _mysql_ResultObject_repr(
 {
     char buf[300];
     snprintf(buf, 300, "<_mysql.result object at %p>", self);
-    return PyString_FromString(buf);
+    return PyUnicode_FromString(buf);
 }
 
 static PyMethodDef _mysql_ConnectionObject_methods[] = {
@@ -2391,13 +2467,9 @@ _mysql_ConnectionObject_getattro(
     PyObject *name)
 {
     const char *cname;
-#ifdef IS_PY3K
     cname = PyUnicode_AsUTF8(name);
-#else
-    cname = PyString_AsString(name);
-#endif
     if (strcmp(cname, "closed") == 0)
-        return PyInt_FromLong((long)!(self->open));
+        return PyLong_FromLong((long)!(self->open));
 
     return PyObject_GenericGetAttr((PyObject *)self, name);
 }
@@ -2638,7 +2710,6 @@ an argument are now methods of the result object. Deprecated functions\n\
 (as of 3.23) are NOT implemented.\n\
 ";
 
-#ifdef IS_PY3K
 static struct PyModuleDef _mysqlmodule = {
    PyModuleDef_HEAD_INIT,
    "_mysql",   /* name of module */
@@ -2650,23 +2721,14 @@ static struct PyModuleDef _mysqlmodule = {
 
 PyMODINIT_FUNC
 PyInit__mysql(void)
-#else
-DL_EXPORT(void)
-init_mysql(void)
-#endif
 {
     PyObject *dict, *module, *emod, *edict;
 
     if (mysql_library_init(0, NULL, NULL)) {
         PyErr_SetString(PyExc_ImportError, "_mysql: mysql_library_init failed");
-#ifdef IS_PY3K
         return NULL;
-#else
-        return;
-#endif
     }
 
-#ifdef IS_PY3K
     if (PyType_Ready(&_mysql_ConnectionObject_Type) < 0)
         return NULL;
     if (PyType_Ready(&_mysql_ResultObject_Type) < 0)
@@ -2674,15 +2736,6 @@ init_mysql(void)
 
     module = PyModule_Create(&_mysqlmodule);
     if (!module) return module; /* this really should never happen */
-#else
-    if (PyType_Ready(&_mysql_ConnectionObject_Type) < 0)
-        return;
-    if (PyType_Ready(&_mysql_ResultObject_Type) < 0)
-        return;
-    module = Py_InitModule4("_mysql", _mysql_methods, _mysql___doc__,
-                (PyObject *)NULL, PYTHON_API_VERSION);
-    if (!module) return; /* this really should never happen */
-#endif
 
     if (!(dict = PyModule_GetDict(module))) goto error;
     if (PyDict_SetItemString(dict, "version_info",
@@ -2690,7 +2743,7 @@ init_mysql(void)
                        dict, dict)))
         goto error;
     if (PyDict_SetItemString(dict, "__version__",
-                   PyString_FromString(QUOTE(__version__))))
+                   PyUnicode_FromString(QUOTE(__version__))))
         goto error;
     if (PyDict_SetItemString(dict, "connection",
                    (PyObject *)&_mysql_ConnectionObject_Type))
@@ -2744,9 +2797,7 @@ init_mysql(void)
         PyErr_SetString(PyExc_ImportError, "_mysql: init failed");
         module = NULL;
     }
-#ifdef IS_PY3K
     return module;
-#endif
 }
 
 /* vim: set ts=4 sts=4 sw=4 expandtab : */
diff --git a/MySQLdb/compat.py b/MySQLdb/compat.py
deleted file mode 100644
index f8d98ac..0000000
--- a/MySQLdb/compat.py
+++ /dev/null
@@ -1,14 +0,0 @@
-import sys
-
-if sys.version_info[0] == 2:
-    PY2 = True
-    unicode = unicode
-    unichr = unichr
-    long = long
-    StandardError = StandardError
-else:
-    PY2 = False
-    unicode = str
-    unichr = chr
-    long = int
-    StandardError = Exception
diff --git a/MySQLdb/connections.py b/MySQLdb/connections.py
index 4c33ec5..3832466 100644
--- a/MySQLdb/connections.py
+++ b/MySQLdb/connections.py
@@ -5,14 +5,19 @@ want to make your own subclasses. In most cases, you will probably
 override Connection.default_cursor with a non-standard Cursor class.
 """
 import re
-import sys
-
-from MySQLdb import cursors, _mysql
-from MySQLdb.compat import unicode, PY2
-from MySQLdb._exceptions import (
-    Warning, Error, InterfaceError, DataError,
-    DatabaseError, OperationalError, IntegrityError, InternalError,
-    NotSupportedError, ProgrammingError,
+
+from . import cursors, _mysql
+from ._exceptions import (
+    Warning,
+    Error,
+    InterfaceError,
+    DataError,
+    DatabaseError,
+    OperationalError,
+    IntegrityError,
+    InternalError,
+    NotSupportedError,
+    ProgrammingError,
 )
 
 # Mapping from MySQL charset name to Python codec name
@@ -26,6 +31,7 @@ _charset_to_encoding = {
 
 re_numeric_part = re.compile(r"^(\d+)")
 
+
 def numeric_part(s):
     """Returns the leading numeric part of a string.
 
@@ -56,9 +62,9 @@ class Connection(_mysql.connection):
         :param str host:        host to connect
         :param str user:        user to connect as
         :param str password:    password to use
-        :param str passwd:      alias of password, for backward compatibility
+        :param str passwd:      alias of password (deprecated)
         :param str database:    database to use
-        :param str db:          alias of database, for backward compatibility
+        :param str db:          alias of database (deprecated)
         :param int port:        TCP/IP port to connect to
         :param str unix_socket: location of unix_socket to use
         :param dict conv:       conversion dictionary, see MySQLdb.converters
@@ -85,14 +91,11 @@ class Connection(_mysql.connection):
             columns are returned as bytes. Unicode objects will always
             be encoded to the connection's character set regardless of
             this setting.
-            Default to False on Python 2 and True on Python 3
-            so that you can always get python `str` object by default.
+            Default to True.
 
         :param str charset:
             If supplied, the connection character set will be changed
             to this character set.
-            On Python 2, this option changes default value of `use_unicode`
-            option from False to True.
 
         :param str auth_plugin:
             If supplied, the connection default authentication plugin will be
@@ -107,6 +110,17 @@ class Connection(_mysql.connection):
         :param int client_flag:
             flags to use or 0 (see MySQL docs or constants/CLIENTS.py)
 
+        :param bool multi_statements:
+            If True, enable multi statements for clients >= 4.1.
+            Defaults to True.
+
+        :param str ssl_mode:
+            specify the security settings for connection to the server;
+            see the MySQL documentation for more details
+            (mysql_option(), MYSQL_OPT_SSL_MODE).
+            Only one of 'DISABLED', 'PREFERRED', 'REQUIRED',
+            'VERIFY_CA', 'VERIFY_IDENTITY' can be specified.
+
         :param dict ssl:
             dictionary or mapping contains SSL connection parameters;
             see the MySQL documentation for more details
@@ -134,13 +148,13 @@ class Connection(_mysql.connection):
 
         kwargs2 = kwargs.copy()
 
-        if 'database' in kwargs2:
-            kwargs2['db'] = kwargs2.pop('database')
-        if 'password' in kwargs2:
-            kwargs2['passwd'] = kwargs2.pop('password')
+        if "db" in kwargs2:
+            kwargs2["database"] = kwargs2.pop("db")
+        if "passwd" in kwargs2:
+            kwargs2["password"] = kwargs2.pop("passwd")
 
-        if 'conv' in kwargs:
-            conv = kwargs['conv']
+        if "conv" in kwargs:
+            conv = kwargs["conv"]
         else:
             conv = conversions
 
@@ -150,51 +164,33 @@ class Connection(_mysql.connection):
                 conv2[k] = v[:]
             else:
                 conv2[k] = v
-        kwargs2['conv'] = conv2
-
-        cursorclass = kwargs2.pop('cursorclass', self.default_cursor)
-        charset = kwargs2.get('charset', '')
-
-        if charset or not PY2:
-            use_unicode = True
-        else:
-            use_unicode = False
-
-        use_unicode = kwargs2.pop('use_unicode', use_unicode)
-        sql_mode = kwargs2.pop('sql_mode', '')
-        self._binary_prefix = kwargs2.pop('binary_prefix', False)
-
-        client_flag = kwargs.get('client_flag', 0)
-        client_version = tuple([ numeric_part(n) for n in _mysql.get_client_info().split('.')[:2] ])
-        if client_version >= (4, 1):
+        kwargs2["conv"] = conv2
+
+        cursorclass = kwargs2.pop("cursorclass", self.default_cursor)
+        charset = kwargs2.get("charset", "")
+        use_unicode = kwargs2.pop("use_unicode", True)
+        sql_mode = kwargs2.pop("sql_mode", "")
+        self._binary_prefix = kwargs2.pop("binary_prefix", False)
+
+        client_flag = kwargs.get("client_flag", 0)
+        client_flag |= CLIENT.MULTI_RESULTS
+        multi_statements = kwargs2.pop("multi_statements", True)
+        if multi_statements:
             client_flag |= CLIENT.MULTI_STATEMENTS
-        if client_version >= (5, 0):
-            client_flag |= CLIENT.MULTI_RESULTS
-
-        kwargs2['client_flag'] = client_flag
+        kwargs2["client_flag"] = client_flag
 
         # PEP-249 requires autocommit to be initially off
-        autocommit = kwargs2.pop('autocommit', False)
+        autocommit = kwargs2.pop("autocommit", False)
 
-        super(Connection, self).__init__(*args, **kwargs2)
+        super().__init__(*args, **kwargs2)
         self.cursorclass = cursorclass
-        self.encoders = dict([ (k, v) for k, v in conv.items()
-                               if type(k) is not int ])
-
-        # XXX THIS IS GARBAGE: While this is just a garbage and undocumented,
-        # Django 1.11 depends on it.  And they don't fix it because
-        # they are in security-only fix mode.
-        # So keep this garbage for now.  This will be removed in 1.5.
-        # See PyMySQL/mysqlclient-python#306
-        self.encoders[bytes] = bytes
+        self.encoders = {k: v for k, v in conv.items() if type(k) is not int}
 
-        self._server_version = tuple([ numeric_part(n) for n in self.get_server_info().split('.')[:2] ])
+        self._server_version = tuple(
+            [numeric_part(n) for n in self.get_server_info().split(".")[:2]]
+        )
 
-        self.encoding = 'ascii'  # overridden in set_character_set()
-        db = proxy(self)
-
-        def unicode_literal(u, dummy=None):
-            return db.string_literal(u.encode(db.encoding))
+        self.encoding = "ascii"  # overridden in set_character_set()
 
         if not charset:
             charset = self.character_set_name()
@@ -204,20 +200,39 @@ class Connection(_mysql.connection):
             self.set_sql_mode(sql_mode)
 
         if use_unicode:
-            for t in (FIELD_TYPE.STRING, FIELD_TYPE.VAR_STRING, FIELD_TYPE.VARCHAR, FIELD_TYPE.TINY_BLOB,
-                      FIELD_TYPE.MEDIUM_BLOB, FIELD_TYPE.LONG_BLOB, FIELD_TYPE.BLOB):
+            for t in (
+                FIELD_TYPE.STRING,
+                FIELD_TYPE.VAR_STRING,
+                FIELD_TYPE.VARCHAR,
+                FIELD_TYPE.TINY_BLOB,
+                FIELD_TYPE.MEDIUM_BLOB,
+                FIELD_TYPE.LONG_BLOB,
+                FIELD_TYPE.BLOB,
+            ):
                 self.converter[t] = _bytes_or_str
             # Unlike other string/blob types, JSON is always text.
             # MySQL may return JSON with charset==binary.
-            self.converter[FIELD_TYPE.JSON] = unicode
+            self.converter[FIELD_TYPE.JSON] = str
+
+        db = proxy(self)
+
+        def unicode_literal(u, dummy=None):
+            return db.string_literal(u.encode(db.encoding))
+
+        self.encoders[str] = unicode_literal
 
-        self.encoders[unicode] = unicode_literal
         self._transactional = self.server_capabilities & CLIENT.TRANSACTIONS
         if self._transactional:
             if autocommit is not None:
                 self.autocommit(autocommit)
         self.messages = []
 
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        self.close()
+
     def autocommit(self, on):
         on = bool(on)
         if self.get_autocommit() != on:
@@ -242,11 +257,11 @@ class Connection(_mysql.connection):
         assert isinstance(bs, (bytes, bytearray))
         x = self.string_literal(bs)  # x is escaped and quoted bytes
         if self._binary_prefix:
-            return b'_binary' + x
+            return b"_binary" + x
         return x
 
     def _tuple_literal(self, t):
-        return b"(%s)" % (b','.join(map(self.literal, t)))
+        return b"(%s)" % (b",".join(map(self.literal, t)))
 
     def literal(self, o):
         """If o is a single object, returns an SQL literal as a string.
@@ -256,20 +271,17 @@ class Connection(_mysql.connection):
         Non-standard. For internal use; do not use this in your
         applications.
         """
-        if isinstance(o, unicode):
+        if isinstance(o, str):
             s = self.string_literal(o.encode(self.encoding))
         elif isinstance(o, bytearray):
             s = self._bytes_literal(o)
         elif isinstance(o, bytes):
-            if PY2:
-                s = self.string_literal(o)
-            else:
-                s = self._bytes_literal(o)
+            s = self._bytes_literal(o)
         elif isinstance(o, (tuple, list)):
             s = self._tuple_literal(o)
         else:
             s = self.escape(o, self.encoders)
-            if isinstance(s, unicode):
+            if isinstance(s, str):
                 s = s.encode(self.encoding)
         assert isinstance(s, bytes)
         return s
@@ -281,32 +293,10 @@ class Connection(_mysql.connection):
         """
         self.query(b"BEGIN")
 
-    if not hasattr(_mysql.connection, 'warning_count'):
-
-        def warning_count(self):
-            """Return the number of warnings generated from the
-            last query. This is derived from the info() method."""
-            info = self.info()
-            if info:
-                return int(info.split()[-1])
-            else:
-                return 0
-
     def set_character_set(self, charset):
-        """Set the connection character set to charset. The character
-        set can only be changed in MySQL-4.1 and newer. If you try
-        to change the character set from the current value in an
-        older version, NotSupportedError will be raised."""
-        py_charset = _charset_to_encoding.get(charset, charset)
-        if self.character_set_name() != charset:
-            try:
-                super(Connection, self).set_character_set(charset)
-            except AttributeError:
-                if self._server_version < (4, 1):
-                    raise NotSupportedError("server is too old to set charset")
-                self.query('SET NAMES %s' % charset)
-                self.store_result()
-        self.encoding = py_charset
+        """Set the connection character set to charset."""
+        super().set_character_set(charset)
+        self.encoding = _charset_to_encoding.get(charset, charset)
 
     def set_sql_mode(self, sql_mode):
         """Set the connection sql_mode. See MySQL documentation for
@@ -321,7 +311,8 @@ class Connection(_mysql.connection):
         sequence of tuples of (Level, Code, Message). This
         is only supported in MySQL-4.1 and up. If your server
         is an earlier version, an empty sequence is returned."""
-        if self._server_version < (4,1): return ()
+        if self._server_version < (4, 1):
+            return ()
         self.query("SHOW WARNINGS")
         r = self.store_result()
         warnings = r.fetch_row(0)
@@ -338,4 +329,5 @@ class Connection(_mysql.connection):
     ProgrammingError = ProgrammingError
     NotSupportedError = NotSupportedError
 
+
 # vim: colorcolumn=100
diff --git a/MySQLdb/constants/CLIENT.py b/MySQLdb/constants/CLIENT.py
index 6559917..35f578c 100644
--- a/MySQLdb/constants/CLIENT.py
+++ b/MySQLdb/constants/CLIENT.py
@@ -20,10 +20,8 @@ CHANGE_USER = 512
 INTERACTIVE = 1024
 SSL = 2048
 IGNORE_SIGPIPE = 4096
-TRANSACTIONS = 8192 # mysql_com.h was WRONG prior to 3.23.35
+TRANSACTIONS = 8192  # mysql_com.h was WRONG prior to 3.23.35
 RESERVED = 16384
 SECURE_CONNECTION = 32768
 MULTI_STATEMENTS = 65536
 MULTI_RESULTS = 131072
-
-
diff --git a/MySQLdb/constants/CR.py b/MySQLdb/constants/CR.py
index 753408e..9d33cf6 100644
--- a/MySQLdb/constants/CR.py
+++ b/MySQLdb/constants/CR.py
@@ -9,16 +9,18 @@ if __name__ == "__main__":
     """
     Usage: python CR.py [/path/to/mysql/errmsg.h ...] >> CR.py
     """
-    import fileinput, re
+    import fileinput
+    import re
+
     data = {}
     error_last = None
     for line in fileinput.input():
-        line = re.sub(r'/\*.*?\*/', '', line)
-        m = re.match(r'^\s*#define\s+CR_([A-Z0-9_]+)\s+(\d+)(\s.*|$)', line)
+        line = re.sub(r"/\*.*?\*/", "", line)
+        m = re.match(r"^\s*#define\s+CR_([A-Z0-9_]+)\s+(\d+)(\s.*|$)", line)
         if m:
             name = m.group(1)
             value = int(m.group(2))
-            if name == 'ERROR_LAST':
+            if name == "ERROR_LAST":
                 if error_last is None or error_last < value:
                     error_last = value
                 continue
@@ -27,9 +29,9 @@ if __name__ == "__main__":
             data[value].add(name)
     for value, names in sorted(data.items()):
         for name in sorted(names):
-            print('%s = %s' % (name, value))
+            print("{} = {}".format(name, value))
     if error_last is not None:
-        print('ERROR_LAST = %s' % error_last)
+        print("ERROR_LAST = %s" % error_last)
 
 
 ERROR_FIRST = 2000
diff --git a/MySQLdb/constants/ER.py b/MySQLdb/constants/ER.py
index 2e623b5..fcd5bf2 100644
--- a/MySQLdb/constants/ER.py
+++ b/MySQLdb/constants/ER.py
@@ -8,18 +8,20 @@ if __name__ == "__main__":
     """
     Usage: python ER.py [/path/to/mysql/mysqld_error.h ...] >> ER.py
     """
-    import fileinput, re
+    import fileinput
+    import re
+
     data = {}
     error_last = None
     for line in fileinput.input():
-        line = re.sub(r'/\*.*?\*/', '', line)
-        m = re.match(r'^\s*#define\s+((ER|WARN)_[A-Z0-9_]+)\s+(\d+)\s*', line)
+        line = re.sub(r"/\*.*?\*/", "", line)
+        m = re.match(r"^\s*#define\s+((ER|WARN)_[A-Z0-9_]+)\s+(\d+)\s*", line)
         if m:
             name = m.group(1)
-            if name.startswith('ER_'):
+            if name.startswith("ER_"):
                 name = name[3:]
             value = int(m.group(3))
-            if name == 'ERROR_LAST':
+            if name == "ERROR_LAST":
                 if error_last is None or error_last < value:
                     error_last = value
                 continue
@@ -28,9 +30,9 @@ if __name__ == "__main__":
             data[value].add(name)
     for value, names in sorted(data.items()):
         for name in sorted(names):
-            print('%s = %s' % (name, value))
+            print("{} = {}".format(name, value))
     if error_last is not None:
-        print('ERROR_LAST = %s' % error_last)
+        print("ERROR_LAST = %s" % error_last)
 
 
 ERROR_FIRST = 1000
diff --git a/MySQLdb/constants/__init__.py b/MySQLdb/constants/__init__.py
index 3e774cd..0372265 100644
--- a/MySQLdb/constants/__init__.py
+++ b/MySQLdb/constants/__init__.py
@@ -1 +1 @@
-__all__ = ['CR', 'FIELD_TYPE','CLIENT','ER','FLAG']
+__all__ = ["CR", "FIELD_TYPE", "CLIENT", "ER", "FLAG"]
diff --git a/MySQLdb/converters.py b/MySQLdb/converters.py
index 645e378..33f22f7 100644
--- a/MySQLdb/converters.py
+++ b/MySQLdb/converters.py
@@ -32,16 +32,24 @@ MySQL.connect().
 """
 from decimal import Decimal
 
-from MySQLdb._mysql import string_literal, escape
+from MySQLdb._mysql import string_literal
 from MySQLdb.constants import FIELD_TYPE, FLAG
-from MySQLdb.times import *
-from MySQLdb.compat import PY2, long, unicode
+from MySQLdb.times import (
+    Date,
+    DateTimeType,
+    DateTime2literal,
+    DateTimeDeltaType,
+    DateTimeDelta2literal,
+    DateTime_or_None,
+    TimeDelta_or_None,
+    Date_or_None,
+)
 from MySQLdb._exceptions import ProgrammingError
 
-NoneType = type(None)
-
 import array
 
+NoneType = type(None)
+
 try:
     ArrayType = array.ArrayType
 except AttributeError:
@@ -49,28 +57,33 @@ except AttributeError:
 
 
 def Bool2Str(s, d):
-    return b'1' if s else b'0'
+    return b"1" if s else b"0"
+
 
 def Set2Str(s, d):
     # Only support ascii string.  Not tested.
-    return string_literal(','.join(s))
+    return string_literal(",".join(s))
+
 
 def Thing2Str(s, d):
     """Convert something into a string via str()."""
     return str(s)
 
+
 def Float2Str(o, d):
     s = repr(o)
-    if s in ('inf', 'nan'):
+    if s in ("inf", "nan"):
         raise ProgrammingError("%s can not be used with MySQL" % s)
-    if 'e' not in s:
-        s += 'e0'
+    if "e" not in s:
+        s += "e0"
     return s
 
+
 def None2NULL(o, d):
     """Convert None to NULL."""
     return b"NULL"
 
+
 def Thing2Literal(o, d):
     """Convert something into a SQL string literal.  If using
     MySQL-3.23 or newer, string_literal() is a method of the
@@ -78,18 +91,20 @@ def Thing2Literal(o, d):
     that method when the connection is created."""
     return string_literal(o)
 
+
 def Decimal2Literal(o, d):
-    return format(o, 'f')
+    return format(o, "f")
+
 
 def array2Str(o, d):
     return Thing2Literal(o.tostring(), d)
 
+
 # bytes or str regarding to BINARY_FLAG.
-_bytes_or_str = ((FLAG.BINARY, bytes), (None, unicode))
+_bytes_or_str = ((FLAG.BINARY, bytes), (None, str))
 
 conversions = {
     int: Thing2Str,
-    long: Thing2Str,
     float: Float2Str,
     NoneType: None2NULL,
     ArrayType: array2Str,
@@ -99,7 +114,6 @@ conversions = {
     DateTimeDeltaType: DateTimeDelta2literal,
     set: Set2Str,
     Decimal: Decimal2Literal,
-
     FIELD_TYPE.TINY: int,
     FIELD_TYPE.SHORT: int,
     FIELD_TYPE.LONG: int,
@@ -114,7 +128,6 @@ conversions = {
     FIELD_TYPE.DATETIME: DateTime_or_None,
     FIELD_TYPE.TIME: TimeDelta_or_None,
     FIELD_TYPE.DATE: Date_or_None,
-
     FIELD_TYPE.TINY_BLOB: bytes,
     FIELD_TYPE.MEDIUM_BLOB: bytes,
     FIELD_TYPE.LONG_BLOB: bytes,
diff --git a/MySQLdb/cursors.py b/MySQLdb/cursors.py
index ee834e4..f8a4864 100644
--- a/MySQLdb/cursors.py
+++ b/MySQLdb/cursors.py
@@ -3,29 +3,27 @@
 This module implements Cursors of various types for MySQLdb. By
 default, MySQLdb uses the Cursor class.
 """
-from __future__ import print_function, absolute_import
-from functools import partial
 import re
-import sys
 
-from .compat import unicode
-from ._exceptions import (
-    Warning, Error, InterfaceError, DataError,
-    DatabaseError, OperationalError, IntegrityError, InternalError,
-    NotSupportedError, ProgrammingError)
+from ._exceptions import ProgrammingError
 
 
 #: Regular expression for :meth:`Cursor.executemany`.
 #: executemany only supports simple bulk insert.
 #: You can use it to load large dataset.
 RE_INSERT_VALUES = re.compile(
-    r"\s*((?:INSERT|REPLACE)\b.+\bVALUES?\s*)" +
-    r"(\(\s*(?:%s|%\(.+\)s)\s*(?:,\s*(?:%s|%\(.+\)s)\s*)*\))" +
-    r"(\s*(?:ON DUPLICATE.*)?);?\s*\Z",
-    re.IGNORECASE | re.DOTALL)
-
-
-class BaseCursor(object):
+    "".join(
+        [
+            r"\s*((?:INSERT|REPLACE)\b.+\bVALUES?\s*)",
+            r"(\(\s*(?:%s|%\(.+\)s)\s*(?:,\s*(?:%s|%\(.+\)s)\s*)*\))",
+            r"(\s*(?:ON DUPLICATE.*)?);?\s*\Z",
+        ]
+    ),
+    re.IGNORECASE | re.DOTALL,
+)
+
+
+class BaseCursor:
     """A base for Cursor classes. Useful attributes:
 
     description
@@ -42,16 +40,24 @@ class BaseCursor(object):
         default number of rows fetchmany() will fetch
     """
 
-    #: Max stetement size which :meth:`executemany` generates.
+    #: Max statement size which :meth:`executemany` generates.
     #:
     #: Max size of allowed statement is max_allowed_packet - packet_header_size.
     #: Default value of max_allowed_packet is 1048576.
-    max_stmt_length = 64*1024
+    max_stmt_length = 64 * 1024
 
     from ._exceptions import (
-        MySQLError, Warning, Error, InterfaceError,
-        DatabaseError, DataError, OperationalError, IntegrityError,
-        InternalError, ProgrammingError, NotSupportedError,
+        MySQLError,
+        Warning,
+        Error,
+        InterfaceError,
+        DatabaseError,
+        DataError,
+        OperationalError,
+        IntegrityError,
+        InternalError,
+        ProgrammingError,
+        NotSupportedError,
     )
 
     connection = None
@@ -64,17 +70,8 @@ class BaseCursor(object):
         self.arraysize = 1
         self._executed = None
 
-        # XXX THIS IS GARBAGE: While this is totally garbage and private,
-        # Django 1.11 depends on it.  And they don't fix it because
-        # they are in security-only fix mode.
-        # So keep this garbage for now.  This will be removed in 1.5.
-        # See PyMySQL/mysqlclient-python#303
-        self._last_executed = None
-
         self.lastrowid = None
-        self.messages = []
         self._result = None
-        self._warnings = None
         self.rownumber = None
         self._rows = None
 
@@ -101,7 +98,7 @@ class BaseCursor(object):
         literal = conn.literal
 
         def ensure_bytes(x):
-            if isinstance(x, unicode):
+            if isinstance(x, str):
                 return x.encode(encoding)
             elif isinstance(x, tuple):
                 return tuple(map(ensure_bytes, x))
@@ -112,8 +109,10 @@ class BaseCursor(object):
         if isinstance(args, (tuple, list)):
             ret = tuple(literal(ensure_bytes(arg)) for arg in args)
         elif isinstance(args, dict):
-            ret = {ensure_bytes(key): literal(ensure_bytes(val))
-                   for (key, val) in args.items()}
+            ret = {
+                ensure_bytes(key): literal(ensure_bytes(val))
+                for (key, val) in args.items()
+            }
         else:
             # If it's not a dictionary let's try escaping it anyways.
             # Worst case it will throw a Value error
@@ -133,7 +132,6 @@ class BaseCursor(object):
         """
         if self._executed:
             self.fetchall()
-        del self.messages[:]
 
         db = self._get_db()
         nr = db.next_result()
@@ -154,7 +152,6 @@ class BaseCursor(object):
         self.rowcount = db.affected_rows()
         self.rownumber = 0
         self.lastrowid = db.insert_id()
-        self._warnings = None
 
     def _post_get_result(self):
         pass
@@ -187,14 +184,14 @@ class BaseCursor(object):
             pass
         db = self._get_db()
 
-        if isinstance(query, unicode):
+        if isinstance(query, str):
             query = query.encode(db.encoding)
 
         if args is not None:
             if isinstance(args, dict):
                 nargs = {}
                 for key, item in args.items():
-                    if isinstance(key, unicode):
+                    if isinstance(key, str):
                         key = key.encode(db.encoding)
                     nargs[key] = db.literal(item)
                 args = nargs
@@ -221,8 +218,6 @@ class BaseCursor(object):
         REPLACE. Otherwise it is equivalent to looping over args with
         execute().
         """
-        del self.messages[:]
-
         if not args:
             return
 
@@ -230,23 +225,30 @@ class BaseCursor(object):
         if m:
             q_prefix = m.group(1) % ()
             q_values = m.group(2).rstrip()
-            q_postfix = m.group(3) or ''
-            assert q_values[0] == '(' and q_values[-1] == ')'
-            return self._do_execute_many(q_prefix, q_values, q_postfix, args,
-                                         self.max_stmt_length,
-                                         self._get_db().encoding)
+            q_postfix = m.group(3) or ""
+            assert q_values[0] == "(" and q_values[-1] == ")"
+            return self._do_execute_many(
+                q_prefix,
+                q_values,
+                q_postfix,
+                args,
+                self.max_stmt_length,
+                self._get_db().encoding,
+            )
 
         self.rowcount = sum(self.execute(query, arg) for arg in args)
         return self.rowcount
 
-    def _do_execute_many(self, prefix, values, postfix, args, max_stmt_length, encoding):
+    def _do_execute_many(
+        self, prefix, values, postfix, args, max_stmt_length, encoding
+    ):
         conn = self._get_db()
         escape = self._escape_args
-        if isinstance(prefix, unicode):
+        if isinstance(prefix, str):
             prefix = prefix.encode(encoding)
-        if isinstance(values, unicode):
+        if isinstance(values, str):
             values = values.encode(encoding)
-        if isinstance(postfix, unicode):
+        if isinstance(postfix, str):
             postfix = postfix.encode(encoding)
         sql = bytearray(prefix)
         args = iter(args)
@@ -259,7 +261,7 @@ class BaseCursor(object):
                 rows += self.execute(sql + postfix)
                 sql = bytearray(prefix)
             else:
-                sql += b','
+                sql += b","
             sql += v
         rows += self.execute(sql + postfix)
         self.rowcount = rows
@@ -294,18 +296,20 @@ class BaseCursor(object):
         disconnected.
         """
         db = self._get_db()
-        if isinstance(procname, unicode):
+        if isinstance(procname, str):
             procname = procname.encode(db.encoding)
         if args:
-            fmt = b'@_' + procname + b'_%d=%s'
-            q = b'SET %s' % b','.join(fmt % (index, db.literal(arg))
-                                      for index, arg in enumerate(args))
+            fmt = b"@_" + procname + b"_%d=%s"
+            q = b"SET %s" % b",".join(
+                fmt % (index, db.literal(arg)) for index, arg in enumerate(args)
+            )
             self._query(q)
             self.nextset()
 
-        q = b"CALL %s(%s)" % (procname,
-                              b','.join([b'@_%s_%d' % (procname, i)
-                                         for i in range(len(args))]))
+        q = b"CALL %s(%s)" % (
+            procname,
+            b",".join([b"@_%s_%d" % (procname, i) for i in range(len(args))]),
+        )
         self._query(q)
         return args
 
@@ -316,7 +320,6 @@ class BaseCursor(object):
         self._do_get_result(db)
         self._post_get_result()
         self._executed = q
-        self._last_executed = q  # XXX THIS IS GARBAGE: See above.
         return self.rowcount
 
     def _fetch_row(self, size=1):
@@ -339,7 +342,7 @@ class BaseCursor(object):
     NotSupportedError = NotSupportedError
 
 
-class CursorStoreResultMixIn(object):
+class CursorStoreResultMixIn:
     """This is a MixIn class which causes the entire result set to be
     stored on the client side, i.e. it uses mysql_store_result(). If the
     result set can be very large, consider adding a LIMIT clause to your
@@ -367,21 +370,21 @@ class CursorStoreResultMixIn(object):
         than size. If size is not defined, cursor.arraysize is used."""
         self._check_executed()
         end = self.rownumber + (size or self.arraysize)
-        result = self._rows[self.rownumber:end]
+        result = self._rows[self.rownumber : end]
         self.rownumber = min(end, len(self._rows))
         return result
 
     def fetchall(self):
-        """Fetchs all available rows from the cursor."""
+        """Fetches all available rows from the cursor."""
         self._check_executed()
         if self.rownumber:
-            result = self._rows[self.rownumber:]
+            result = self._rows[self.rownumber :]
         else:
             result = self._rows
         self.rownumber = len(self._rows)
         return result
 
-    def scroll(self, value, mode='relative'):
+    def scroll(self, value, mode="relative"):
         """Scroll the cursor in the result set to a new position according
         to mode.
 
@@ -389,9 +392,9 @@ class CursorStoreResultMixIn(object):
         the current position in the result set, if set to 'absolute',
         value states an absolute target position."""
         self._check_executed()
-        if mode == 'relative':
+        if mode == "relative":
             r = self.rownumber + value
-        elif mode == 'absolute':
+        elif mode == "absolute":
             r = value
         else:
             raise ProgrammingError("unknown scroll mode %s" % repr(mode))
@@ -401,11 +404,11 @@ class CursorStoreResultMixIn(object):
 
     def __iter__(self):
         self._check_executed()
-        result = self.rownumber and self._rows[self.rownumber:] or self._rows
+        result = self.rownumber and self._rows[self.rownumber :] or self._rows
         return iter(result)
 
 
-class CursorUseResultMixIn(object):
+class CursorUseResultMixIn:
 
     """This is a MixIn class which causes the result set to be stored
     in the server and sent row-by-row to client side, i.e. it uses
@@ -434,7 +437,7 @@ class CursorUseResultMixIn(object):
         return r
 
     def fetchall(self):
-        """Fetchs all available rows from the cursor."""
+        """Fetches all available rows from the cursor."""
         self._check_executed()
         r = self._fetch_row(0)
         self.rownumber = self.rownumber + len(r)
@@ -452,39 +455,35 @@ class CursorUseResultMixIn(object):
     __next__ = next
 
 
-class CursorTupleRowsMixIn(object):
+class CursorTupleRowsMixIn:
     """This is a MixIn class that causes all rows to be returned as tuples,
     which is the standard form required by DB API."""
 
     _fetch_type = 0
 
 
-class CursorDictRowsMixIn(object):
+class CursorDictRowsMixIn:
     """This is a MixIn class that causes all rows to be returned as
     dictionaries. This is a non-standard feature."""
 
     _fetch_type = 1
 
 
-class Cursor(CursorStoreResultMixIn, CursorTupleRowsMixIn,
-             BaseCursor):
+class Cursor(CursorStoreResultMixIn, CursorTupleRowsMixIn, BaseCursor):
     """This is the standard Cursor class that returns rows as tuples
     and stores the result set in the client."""
 
 
-class DictCursor(CursorStoreResultMixIn, CursorDictRowsMixIn,
-                 BaseCursor):
+class DictCursor(CursorStoreResultMixIn, CursorDictRowsMixIn, BaseCursor):
     """This is a Cursor class that returns rows as dictionaries and
     stores the result set in the client."""
 
 
-class SSCursor(CursorUseResultMixIn, CursorTupleRowsMixIn,
-               BaseCursor):
+class SSCursor(CursorUseResultMixIn, CursorTupleRowsMixIn, BaseCursor):
     """This is a Cursor class that returns rows as tuples and stores
     the result set in the server."""
 
 
-class SSDictCursor(CursorUseResultMixIn, CursorDictRowsMixIn,
-                   BaseCursor):
+class SSDictCursor(CursorUseResultMixIn, CursorDictRowsMixIn, BaseCursor):
     """This is a Cursor class that returns rows as dictionaries and
     stores the result set in the server."""
diff --git a/MySQLdb/release.py b/MySQLdb/release.py
index 5d0fa11..46a5e3b 100644
--- a/MySQLdb/release.py
+++ b/MySQLdb/release.py
@@ -1,4 +1,4 @@
 
 __author__ = "Inada Naoki <songofacandy@gmail.com>"
-version_info = (1,4,6,'final',0)
-__version__ = "1.4.6"
+version_info = (2,1,1,'final',0)
+__version__ = "2.1.1"
diff --git a/MySQLdb/times.py b/MySQLdb/times.py
index d47c8fb..915d827 100644
--- a/MySQLdb/times.py
+++ b/MySQLdb/times.py
@@ -16,34 +16,50 @@ Timestamp = datetime
 DateTimeDeltaType = timedelta
 DateTimeType = datetime
 
+
 def DateFromTicks(ticks):
     """Convert UNIX ticks into a date instance."""
     return date(*localtime(ticks)[:3])
 
+
 def TimeFromTicks(ticks):
     """Convert UNIX ticks into a time instance."""
     return time(*localtime(ticks)[3:6])
 
+
 def TimestampFromTicks(ticks):
     """Convert UNIX ticks into a datetime instance."""
     return datetime(*localtime(ticks)[:6])
 
+
 format_TIME = format_DATE = str
 
+
 def format_TIMEDELTA(v):
     seconds = int(v.seconds) % 60
     minutes = int(v.seconds // 60) % 60
     hours = int(v.seconds // 3600) % 24
-    return '%d %d:%d:%d' % (v.days, hours, minutes, seconds)
+    return "%d %d:%d:%d" % (v.days, hours, minutes, seconds)
+
 
 def format_TIMESTAMP(d):
     """
     :type d: datetime.datetime
     """
     if d.microsecond:
-        fmt = "{0.year:04}-{0.month:02}-{0.day:02} {0.hour:02}:{0.minute:02}:{0.second:02}.{0.microsecond:06}"
+        fmt = " ".join(
+            [
+                "{0.year:04}-{0.month:02}-{0.day:02}",
+                "{0.hour:02}:{0.minute:02}:{0.second:02}.{0.microsecond:06}",
+            ]
+        )
     else:
-        fmt = "{0.year:04}-{0.month:02}-{0.day:02} {0.hour:02}:{0.minute:02}:{0.second:02}"
+        fmt = " ".join(
+            [
+                "{0.year:04}-{0.month:02}-{0.day:02}",
+                "{0.hour:02}:{0.minute:02}:{0.second:02}",
+            ]
+        )
     return fmt.format(d)
 
 
@@ -64,32 +80,32 @@ def DateTime_or_None(s):
             return None
 
         return datetime(
-            int(s[:4]),          # year
-            int(s[5:7]),         # month
-            int(s[8:10]),        # day
+            int(s[:4]),  # year
+            int(s[5:7]),  # month
+            int(s[8:10]),  # day
             int(s[11:13] or 0),  # hour
             int(s[14:16] or 0),  # minute
             int(s[17:19] or 0),  # second
-            micros,              # microsecond
+            micros,  # microsecond
         )
     except ValueError:
         return None
 
+
 def TimeDelta_or_None(s):
     try:
-        h, m, s = s.split(':')
-        if '.' in s:
-            s, ms = s.split('.')
-            ms = ms.ljust(6, '0')
+        h, m, s = s.split(":")
+        if "." in s:
+            s, ms = s.split(".")
+            ms = ms.ljust(6, "0")
         else:
             ms = 0
-        if h[0] == '-':
+        if h[0] == "-":
             negative = True
         else:
             negative = False
         h, m, s, ms = abs(int(h)), int(m), int(s), int(ms)
-        td = timedelta(hours=h, minutes=m, seconds=s,
-                       microseconds=ms)
+        td = timedelta(hours=h, minutes=m, seconds=s, microseconds=ms)
         if negative:
             return -td
         else:
@@ -98,34 +114,37 @@ def TimeDelta_or_None(s):
         # unpacking or int/float conversion failed
         return None
 
+
 def Time_or_None(s):
     try:
-        h, m, s = s.split(':')
-        if '.' in s:
-            s, ms = s.split('.')
-            ms = ms.ljust(6, '0')
+        h, m, s = s.split(":")
+        if "." in s:
+            s, ms = s.split(".")
+            ms = ms.ljust(6, "0")
         else:
             ms = 0
         h, m, s, ms = int(h), int(m), int(s), int(ms)
-        return time(hour=h, minute=m, second=s,
-                    microsecond=ms)
+        return time(hour=h, minute=m, second=s, microsecond=ms)
     except ValueError:
         return None
 
+
 def Date_or_None(s):
     try:
         return date(
-            int(s[:4]),    # year
-            int(s[5:7]),   # month
-            int(s[8:10]),  # day
-        )
+            int(s[:4]),
+            int(s[5:7]),
+            int(s[8:10]),
+        )  # year  # month  # day
     except ValueError:
         return None
 
+
 def DateTime2literal(d, c):
     """Format a DateTime object as an ISO timestamp."""
     return string_literal(format_TIMESTAMP(d))
 
+
 def DateTimeDelta2literal(d, c):
     """Format a DateTimeDelta object as a time."""
     return string_literal(format_TIMEDELTA(d))
diff --git a/PKG-INFO b/PKG-INFO
index d44e5a1..be5c83f 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,57 +1,11 @@
 Metadata-Version: 2.1
 Name: mysqlclient
-Version: 1.4.6
+Version: 2.1.1
 Summary: Python interface to MySQL
-Home-page: https://github.com/PyMySQL/mysqlclient-python
+Home-page: https://github.com/PyMySQL/mysqlclient
 Author: Inada Naoki
 Author-email: songofacandy@gmail.com
 License: GPL
-Description: # mysqlclient
-        
-        [![Build Status](https://secure.travis-ci.org/PyMySQL/mysqlclient-python.png)](http://travis-ci.org/PyMySQL/mysqlclient-python)
-        
-        This is a fork of [MySQLdb1](https://github.com/farcepest/MySQLdb1).
-        
-        This project adds Python 3 support and bug fixes.
-        I hope this fork is merged back to MySQLdb1 like distribute was merged back to setuptools.
-        
-        ## Install
-        
-        ### Prerequisites
-        
-        You may need to install the Python and MySQL development headers and libraries like so:
-        
-        * `sudo apt-get install python-dev default-libmysqlclient-dev`  # Debian / Ubuntu
-        * `sudo yum install python-devel mysql-devel`  # Red Hat / CentOS
-        * `brew install mysql-client`  # macOS (Homebrew)
-        
-        On Windows, there are binary wheels you can install without MySQLConnector/C or MSVC.
-        
-        #### Note on Python 3 : if you are using python3 then you need to install python3-dev using the following command :
-        
-        `sudo apt-get install python3-dev` # debian / Ubuntu
-        
-        `sudo yum install python3-devel `  # Red Hat / CentOS
-        
-        ### Install from PyPI
-        
-        `pip install mysqlclient`
-        
-        NOTE: Wheels for Windows may be not released with source package. You should pin version
-        in your `requirements.txt` to avoid trying to install newest source package.
-        
-        
-        ### Install from source
-        
-        1. Download source by `git clone` or [zipfile](https://github.com/PyMySQL/mysqlclient-python/archive/master.zip).
-        2. Customize `site.cfg`
-        3. `python setup.py install`
-        
-        ### Documentation
-        
-        Documentation is hosted on [Read The Docs](https://mysqlclient.readthedocs.io/)
-        
-        
 Platform: ALL
 Classifier: Development Status :: 5 - Production/Stable
 Classifier: Environment :: Other Environment
@@ -64,13 +18,115 @@ Classifier: Operating System :: POSIX :: Linux
 Classifier: Operating System :: Unix
 Classifier: Programming Language :: C
 Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 2
-Classifier: Programming Language :: Python :: 2.7
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.5
 Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
 Classifier: Topic :: Database
 Classifier: Topic :: Database :: Database Engines/Servers
+Requires-Python: >=3.5
 Description-Content-Type: text/markdown
+License-File: LICENSE
+
+# mysqlclient
+
+This project is a fork of [MySQLdb1](https://github.com/farcepest/MySQLdb1).
+This project adds Python 3 support and fixed many bugs.
+
+* PyPI: https://pypi.org/project/mysqlclient/
+* GitHub: https://github.com/PyMySQL/mysqlclient
+
+
+## Support
+
+**Do Not use Github Issue Tracker to ask help.  OSS Maintainer is not free tech support**
+
+When your question looks relating to Python rather than MySQL:
+
+* Python mailing list [python-list](https://mail.python.org/mailman/listinfo/python-list)
+* Slack [pythondev.slack.com](https://pyslackers.com/web/slack)
+
+Or when you have question about MySQL:
+
+* [MySQL Community on Slack](https://lefred.be/mysql-community-on-slack/)
+
+
+## Install
+
+### Windows
+
+Building mysqlclient on Windows is very hard.
+But there are some binary wheels you can install easily.
+
+If binary wheels do not exist for your version of Python, it may be possible to
+build from source, but if this does not work, **do not come asking for support.**
+To build from source, download the
+[MariaDB C Connector](https://mariadb.com/downloads/#connectors) and install
+it. It must be installed in the default location
+(usually "C:\Program Files\MariaDB\MariaDB Connector C" or
+"C:\Program Files (x86)\MariaDB\MariaDB Connector C" for 32-bit). If you
+build the connector yourself or install it in a different location, set the
+environment variable `MYSQLCLIENT_CONNECTOR` before installing. Once you have
+the connector installed and an appropriate version of Visual Studio for your
+version of Python:
+
+```
+$ pip install mysqlclient
+```
+
+### macOS (Homebrew)
+
+Install MySQL and mysqlclient:
+
+```
+# Assume you are activating Python 3 venv
+$ brew install mysql
+$ pip install mysqlclient
+```
+
+If you don't want to install MySQL server, you can use mysql-client instead:
+
+```
+# Assume you are activating Python 3 venv
+$ brew install mysql-client
+$ echo 'export PATH="/usr/local/opt/mysql-client/bin:$PATH"' >> ~/.bash_profile
+$ export PATH="/usr/local/opt/mysql-client/bin:$PATH"
+$ pip install mysqlclient
+```
+
+### Linux
+
+**Note that this is a basic step.  I can not support complete step for build for all
+environment.  If you can see some error, you should fix it by yourself, or ask for
+support in some user forum.  Don't file a issue on the issue tracker.**
+
+You may need to install the Python 3 and MySQL development headers and libraries like so:
+
+* `$ sudo apt-get install python3-dev default-libmysqlclient-dev build-essential`  # Debian / Ubuntu
+* `% sudo yum install python3-devel mysql-devel`  # Red Hat / CentOS
+
+Then you can install mysqlclient via pip now:
+
+```
+$ pip install mysqlclient
+```
+
+### Customize build (POSIX)
+
+mysqlclient uses `mysql_config` or `mariadb_config` by default for finding
+compiler/linker flags.
+
+You can use `MYSQLCLIENT_CFLAGS` and `MYSQLCLIENT_LDFLAGS` environment
+variables to customize compiler/linker options.
+
+```
+$ export MYSQLCLIENT_CFLAGS=`pkg-config mysqlclient --cflags`
+$ export MYSQLCLIENT_LDFLAGS=`pkg-config mysqlclient --libs`
+$ pip install mysqlclient
+```
+
+### Documentation
+
+Documentation is hosted on [Read The Docs](https://mysqlclient.readthedocs.io/)
diff --git a/README.md b/README.md
index 080d5fd..4dbc54d 100644
--- a/README.md
+++ b/README.md
@@ -1,45 +1,100 @@
 # mysqlclient
 
-[![Build Status](https://secure.travis-ci.org/PyMySQL/mysqlclient-python.png)](http://travis-ci.org/PyMySQL/mysqlclient-python)
+This project is a fork of [MySQLdb1](https://github.com/farcepest/MySQLdb1).
+This project adds Python 3 support and fixed many bugs.
 
-This is a fork of [MySQLdb1](https://github.com/farcepest/MySQLdb1).
+* PyPI: https://pypi.org/project/mysqlclient/
+* GitHub: https://github.com/PyMySQL/mysqlclient
+
+
+## Support
+
+**Do Not use Github Issue Tracker to ask help.  OSS Maintainer is not free tech support**
+
+When your question looks relating to Python rather than MySQL:
+
+* Python mailing list [python-list](https://mail.python.org/mailman/listinfo/python-list)
+* Slack [pythondev.slack.com](https://pyslackers.com/web/slack)
+
+Or when you have question about MySQL:
+
+* [MySQL Community on Slack](https://lefred.be/mysql-community-on-slack/)
 
-This project adds Python 3 support and bug fixes.
-I hope this fork is merged back to MySQLdb1 like distribute was merged back to setuptools.
 
 ## Install
 
-### Prerequisites
+### Windows
 
-You may need to install the Python and MySQL development headers and libraries like so:
+Building mysqlclient on Windows is very hard.
+But there are some binary wheels you can install easily.
 
-* `sudo apt-get install python-dev default-libmysqlclient-dev`  # Debian / Ubuntu
-* `sudo yum install python-devel mysql-devel`  # Red Hat / CentOS
-* `brew install mysql-client`  # macOS (Homebrew)
+If binary wheels do not exist for your version of Python, it may be possible to
+build from source, but if this does not work, **do not come asking for support.**
+To build from source, download the
+[MariaDB C Connector](https://mariadb.com/downloads/#connectors) and install
+it. It must be installed in the default location
+(usually "C:\Program Files\MariaDB\MariaDB Connector C" or
+"C:\Program Files (x86)\MariaDB\MariaDB Connector C" for 32-bit). If you
+build the connector yourself or install it in a different location, set the
+environment variable `MYSQLCLIENT_CONNECTOR` before installing. Once you have
+the connector installed and an appropriate version of Visual Studio for your
+version of Python:
 
-On Windows, there are binary wheels you can install without MySQLConnector/C or MSVC.
+```
+$ pip install mysqlclient
+```
 
-#### Note on Python 3 : if you are using python3 then you need to install python3-dev using the following command :
+### macOS (Homebrew)
 
-`sudo apt-get install python3-dev` # debian / Ubuntu
+Install MySQL and mysqlclient:
 
-`sudo yum install python3-devel `  # Red Hat / CentOS
+```
+# Assume you are activating Python 3 venv
+$ brew install mysql
+$ pip install mysqlclient
+```
 
-### Install from PyPI
+If you don't want to install MySQL server, you can use mysql-client instead:
 
-`pip install mysqlclient`
+```
+# Assume you are activating Python 3 venv
+$ brew install mysql-client
+$ echo 'export PATH="/usr/local/opt/mysql-client/bin:$PATH"' >> ~/.bash_profile
+$ export PATH="/usr/local/opt/mysql-client/bin:$PATH"
+$ pip install mysqlclient
+```
 
-NOTE: Wheels for Windows may be not released with source package. You should pin version
-in your `requirements.txt` to avoid trying to install newest source package.
+### Linux
 
+**Note that this is a basic step.  I can not support complete step for build for all
+environment.  If you can see some error, you should fix it by yourself, or ask for
+support in some user forum.  Don't file a issue on the issue tracker.**
 
-### Install from source
+You may need to install the Python 3 and MySQL development headers and libraries like so:
 
-1. Download source by `git clone` or [zipfile](https://github.com/PyMySQL/mysqlclient-python/archive/master.zip).
-2. Customize `site.cfg`
-3. `python setup.py install`
+* `$ sudo apt-get install python3-dev default-libmysqlclient-dev build-essential`  # Debian / Ubuntu
+* `% sudo yum install python3-devel mysql-devel`  # Red Hat / CentOS
+
+Then you can install mysqlclient via pip now:
+
+```
+$ pip install mysqlclient
+```
+
+### Customize build (POSIX)
+
+mysqlclient uses `mysql_config` or `mariadb_config` by default for finding
+compiler/linker flags.
+
+You can use `MYSQLCLIENT_CFLAGS` and `MYSQLCLIENT_LDFLAGS` environment
+variables to customize compiler/linker options.
+
+```
+$ export MYSQLCLIENT_CFLAGS=`pkg-config mysqlclient --cflags`
+$ export MYSQLCLIENT_LDFLAGS=`pkg-config mysqlclient --libs`
+$ pip install mysqlclient
+```
 
 ### Documentation
 
 Documentation is hosted on [Read The Docs](https://mysqlclient.readthedocs.io/)
-
diff --git a/debian/changelog b/debian/changelog
index c9f80e8..96db858 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+python-mysqldb (2.1.1-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Wed, 25 Jan 2023 01:18:03 -0000
+
 python-mysqldb (1.4.6-2) unstable; urgency=medium
 
   [ Debian Janitor ]
diff --git a/doc/MySQLdb.constants.rst b/doc/MySQLdb.constants.rst
index 5c9a538..a803e3e 100644
--- a/doc/MySQLdb.constants.rst
+++ b/doc/MySQLdb.constants.rst
@@ -48,4 +48,3 @@ constants Package
     :members:
     :undoc-members:
     :show-inheritance:
-
diff --git a/doc/conf.py b/doc/conf.py
index fc7c089..5d8cd1a 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 #
 # MySQLdb documentation build configuration file, created by
 # sphinx-quickstart on Sun Oct 07 19:36:17 2012.
@@ -11,214 +10,211 @@
 # All configuration values have a default; values that are commented out
 # serve to show the default.
 
-import sys, os
+# skip flake8 and black for this file
+# flake8: noqa
+import sys
+import os
 
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
-#sys.path.insert(0, os.path.abspath('..'))
+# sys.path.insert(0, os.path.abspath(".."))
 
 # -- General configuration -----------------------------------------------------
 
 # If your documentation needs a minimal Sphinx version, state it here.
-#needs_sphinx = '1.0'
+# needs_sphinx = "1.0"
 
 # Add any Sphinx extension module names here, as strings. They can be extensions
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc']
+extensions = ["sphinx.ext.autodoc"]
 
 # Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
 
 # The suffix of source filenames.
-source_suffix = '.rst'
+source_suffix = ".rst"
 
 # The encoding of source files.
-#source_encoding = 'utf-8-sig'
+# source_encoding = "utf-8-sig"
 
 # The master toctree document.
-master_doc = 'index'
+master_doc = "index"
 
 # General information about the project.
-project = u'MySQLdb'
-copyright = u'2012, Andy Dustman'
+project = "MySQLdb"
+copyright = "2012, Andy Dustman"
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
 # built documents.
 #
 # The short X.Y version.
-version = '1.2'
+version = "1.2"
 # The full version, including alpha/beta/rc tags.
-release = '1.2.4b4'
+release = "1.2.4b4"
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
-#language = None
+# language = None
 
 # There are two options for replacing |today|: either, you set today to some
 # non-false value, then it is used:
-#today = ''
+# today = ""
 # Else, today_fmt is used as the format for a strftime call.
-#today_fmt = '%B %d, %Y'
+# today_fmt = "%B %d, %Y"
 
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
-exclude_patterns = ['_build']
+exclude_patterns = ["_build"]
 
 # The reST default role (used for this markup: `text`) to use for all documents.
-#default_role = None
+# default_role = None
 
 # If true, '()' will be appended to :func: etc. cross-reference text.
-#add_function_parentheses = True
+# add_function_parentheses = True
 
 # If true, the current module name will be prepended to all description
 # unit titles (such as .. function::).
-#add_module_names = True
+# add_module_names = True
 
 # If true, sectionauthor and moduleauthor directives will be shown in the
 # output. They are ignored by default.
-#show_authors = False
+# show_authors = False
 
 # The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+pygments_style = "sphinx"
 
 # A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
+# modindex_common_prefix = []
 
 
 # -- Options for HTML output ---------------------------------------------------
 
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
-html_theme = 'default'
+html_theme = "default"
 
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
 # documentation.
-#html_theme_options = {}
+# html_theme_options = {}
 
 # Add any paths that contain custom themes here, relative to this directory.
-#html_theme_path = []
+# html_theme_path = []
 
 # The name for this set of Sphinx documents.  If None, it defaults to
 # "<project> v<release> documentation".
-#html_title = None
+# html_title = None
 
 # A shorter title for the navigation bar.  Default is the same as html_title.
-#html_short_title = None
+# html_short_title = None
 
 # The name of an image file (relative to this directory) to place at the top
 # of the sidebar.
-#html_logo = None
+# html_logo = None
 
 # The name of an image file (within the static path) to use as favicon of the
 # docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
 # pixels large.
-#html_favicon = None
+# html_favicon = None
 
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+html_static_path = ["_static"]
 
 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 # using the given strftime format.
-#html_last_updated_fmt = '%b %d, %Y'
+# html_last_updated_fmt = "%b %d, %Y"
 
 # If true, SmartyPants will be used to convert quotes and dashes to
 # typographically correct entities.
-#html_use_smartypants = True
+# html_use_smartypants = True
 
 # Custom sidebar templates, maps document names to template names.
-#html_sidebars = {}
+# html_sidebars = {}
 
 # Additional templates that should be rendered to pages, maps page names to
 # template names.
-#html_additional_pages = {}
+# html_additional_pages = {}
 
 # If false, no module index is generated.
-#html_domain_indices = True
+# html_domain_indices = True
 
 # If false, no index is generated.
-#html_use_index = True
+# html_use_index = True
 
 # If true, the index is split into individual pages for each letter.
-#html_split_index = False
+# html_split_index = False
 
 # If true, links to the reST sources are added to the pages.
-#html_show_sourcelink = True
+# html_show_sourcelink = True
 
 # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
-#html_show_sphinx = True
+# html_show_sphinx = True
 
 # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-#html_show_copyright = True
+# html_show_copyright = True
 
 # If true, an OpenSearch description file will be output, and all pages will
 # contain a <link> tag referring to it.  The value of this option must be the
 # base URL from which the finished HTML is served.
-#html_use_opensearch = ''
+# html_use_opensearch = ""
 
 # This is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = None
+# html_file_suffix = None
 
 # Output file base name for HTML help builder.
-htmlhelp_basename = 'MySQLdbdoc'
+htmlhelp_basename = "MySQLdbdoc"
 
 
 # -- Options for LaTeX output --------------------------------------------------
 
 latex_elements = {
-# The paper size ('letterpaper' or 'a4paper').
-#'papersize': 'letterpaper',
-
-# The font size ('10pt', '11pt' or '12pt').
-#'pointsize': '10pt',
-
-# Additional stuff for the LaTeX preamble.
-#'preamble': '',
+    # The paper size ('letterpaper' or 'a4paper').
+    #'papersize': 'letterpaper',
+    # The font size ('10pt', '11pt' or '12pt').
+    #'pointsize': '10pt',
+    # Additional stuff for the LaTeX preamble.
+    #'preamble': '',
 }
 
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title, author, documentclass [howto/manual]).
 latex_documents = [
-  ('index', 'MySQLdb.tex', u'MySQLdb Documentation',
-   u'Andy Dustman', 'manual'),
+    ("index", "MySQLdb.tex", "MySQLdb Documentation", "Andy Dustman", "manual"),
 ]
 
 # The name of an image file (relative to this directory) to place at the top of
 # the title page.
-#latex_logo = None
+# latex_logo = None
 
 # For "manual" documents, if this is true, then toplevel headings are parts,
 # not chapters.
-#latex_use_parts = False
+# latex_use_parts = False
 
 # If true, show page references after internal links.
-#latex_show_pagerefs = False
+# latex_show_pagerefs = False
 
 # If true, show URL addresses after external links.
-#latex_show_urls = False
+# latex_show_urls = False
 
 # Documents to append as an appendix to all manuals.
-#latex_appendices = []
+# latex_appendices = []
 
 # If false, no module index is generated.
-#latex_domain_indices = True
+# latex_domain_indices = True
 
 
 # -- Options for manual page output --------------------------------------------
 
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
-man_pages = [
-    ('index', 'mysqldb', u'MySQLdb Documentation',
-     [u'Andy Dustman'], 1)
-]
+man_pages = [("index", "mysqldb", "MySQLdb Documentation", ["Andy Dustman"], 1)]
 
 # If true, show URL addresses after external links.
-#man_show_urls = False
+# man_show_urls = False
 
 
 # -- Options for Texinfo output ------------------------------------------------
@@ -227,16 +223,22 @@ man_pages = [
 # (source start file, target name, title, author,
 #  dir menu entry, description, category)
 texinfo_documents = [
-  ('index', 'MySQLdb', u'MySQLdb Documentation',
-   u'Andy Dustman', 'MySQLdb', 'One line description of project.',
-   'Miscellaneous'),
+    (
+        "index",
+        "MySQLdb",
+        "MySQLdb Documentation",
+        "Andy Dustman",
+        "MySQLdb",
+        "One line description of project.",
+        "Miscellaneous",
+    ),
 ]
 
 # Documents to append as an appendix to all manuals.
-#texinfo_appendices = []
+# texinfo_appendices = []
 
 # If false, no module index is generated.
-#texinfo_domain_indices = True
+# texinfo_domain_indices = True
 
 # How to display URL addresses: 'footnote', 'no', or 'inline'.
-#texinfo_show_urls = 'footnote'
+# texinfo_show_urls = 'footnote'
diff --git a/doc/user_guide.rst b/doc/user_guide.rst
index 0d31777..555adf1 100644
--- a/doc/user_guide.rst
+++ b/doc/user_guide.rst
@@ -125,19 +125,19 @@ We haven't even begun to touch upon all the parameters ``connect()``
 can take.  For this reason, I prefer to use keyword parameters::
 
     db=_mysql.connect(host="localhost",user="joebob",
-                      passwd="moonpie",db="thangs")
+                      password="moonpie",database="thangs")
 
 This does exactly what the last example did, but is arguably easier to
 read. But since the default host is "localhost", and if your login
 name really was "joebob", you could shorten it to this::
 
-    db=_mysql.connect(passwd="moonpie",db="thangs")
+    db=_mysql.connect(password="moonpie",database="thangs")
 
 UNIX sockets and named pipes don't work over a network, so if you
 specify a host other than localhost, TCP will be used, and you can
 specify an odd port if you need to (the default port is 3306)::
 
-    db=_mysql.connect(host="outhouse",port=3307,passwd="moonpie",db="thangs")
+    db=_mysql.connect(host="outhouse",port=3307,password="moonpie",database="thangs")
 
 If you really had to, you could connect to the local host with TCP by
 specifying the full host name, or 127.0.0.1.
@@ -145,7 +145,7 @@ specifying the full host name, or 127.0.0.1.
 Generally speaking, putting passwords in your code is not such a good
 idea::
 
-    db=_mysql.connect(host="outhouse",db="thangs",read_default_file="~/.my.cnf")
+    db=_mysql.connect(host="outhouse",database="thangs",read_default_file="~/.my.cnf")
 
 This does what the previous example does, but gets the username and
 password and other parameters from ~/.my.cnf (UNIX-like systems). Read
@@ -277,10 +277,10 @@ connect(parameters...)
          user
             user to authenticate as. Default: current effective user.
 
-         passwd
+         password
             password to authenticate with. Default: no password.
 
-         db
+         database
             database to use. Default: no default database.
 
          port
@@ -357,6 +357,18 @@ connect(parameters...)
 
             *This must be a keyword parameter.*
 
+         ssl_mode
+            If present, specify the security settings for the
+            connection to the server. For more information on ssl_mode,
+            see the MySQL documentation. Only one of 'DISABLED',
+            'PREFERRED', 'REQUIRED', 'VERIFY_CA', 'VERIFY_IDENTITY'
+            can be specified.
+
+            If not present, the session ssl_mode will be unchanged,
+            but in version 5.7 and later, the default is PREFERRED.
+
+            *This must be a keyword parameter.*
+
          ssl
             This parameter takes a dictionary or mapping, where the
             keys are parameter names used by the mysql_ssl_set_ MySQL
@@ -499,7 +511,7 @@ callproc(procname, args)
       can only be returned with a SELECT statement. Since a stored
       procedure may return zero or more result sets, it is impossible
       for MySQLdb to determine if there are result sets to fetch
-      before the modified parmeters are accessible.
+      before the modified parameters are accessible.
 
       The parameters are stored in the server as @_*procname*_*n*,
       where *n* is the position of the parameter. I.e., if you
@@ -549,7 +561,7 @@ Some examples
 The ``connect()`` method works nearly the same as with `MySQLDB._mysql`_::
 
     import MySQLdb
-    db=MySQLdb.connect(passwd="moonpie",db="thangs")
+    db=MySQLdb.connect(password="moonpie",database="thangs")
 
 To perform a query, you first need a cursor, and then you can execute
 queries on it::
@@ -662,10 +674,9 @@ CursorDictRowsMixIn
 
 Cursor
     The default cursor class. This class is composed of
-    ``CursorWarningMixIn``, ``CursorStoreResultMixIn``,
-    ``CursorTupleRowsMixIn,`` and ``BaseCursor``, i.e. it raises
-    ``Warning``, uses ``mysql_store_result()``, and returns rows as
-    tuples.
+    ``CursorStoreResultMixIn``, ``CursorTupleRowsMixIn``, and
+    ``BaseCursor``, i.e. uses ``mysql_store_result()`` and returns
+    rows as tuples.
 
 DictCursor
     Like ``Cursor`` except it returns rows as dictionaries.
diff --git a/metadata.cfg b/metadata.cfg
index 0b9d55e..5bf1e81 100644
--- a/metadata.cfg
+++ b/metadata.cfg
@@ -1,12 +1,12 @@
 [metadata]
-version: 1.4.6
-version_info: (1,4,6,'final',0)
+version: 2.1.1
+version_info: (2,1,1,'final',0)
 description: Python interface to MySQL
 author: Inada Naoki
 author_email: songofacandy@gmail.com
 license: GPL
 platforms: ALL
-url: https://github.com/PyMySQL/mysqlclient-python
+url: https://github.com/PyMySQL/mysqlclient
 classifiers:
         Development Status :: 5 - Production/Stable
         Environment :: Other Environment
@@ -19,18 +19,16 @@ classifiers:
         Operating System :: Unix
         Programming Language :: C
         Programming Language :: Python
-        Programming Language :: Python :: 2
-        Programming Language :: Python :: 2.7
         Programming Language :: Python :: 3
-        Programming Language :: Python :: 3.5
         Programming Language :: Python :: 3.6
         Programming Language :: Python :: 3.7
         Programming Language :: Python :: 3.8
+        Programming Language :: Python :: 3.9
+        Programming Language :: Python :: 3.10
         Topic :: Database
         Topic :: Database :: Database Engines/Servers
 py_modules:
         MySQLdb._exceptions
-        MySQLdb.compat
         MySQLdb.connections
         MySQLdb.converters
         MySQLdb.cursors
diff --git a/mysqlclient.egg-info/PKG-INFO b/mysqlclient.egg-info/PKG-INFO
index d44e5a1..be5c83f 100644
--- a/mysqlclient.egg-info/PKG-INFO
+++ b/mysqlclient.egg-info/PKG-INFO
@@ -1,57 +1,11 @@
 Metadata-Version: 2.1
 Name: mysqlclient
-Version: 1.4.6
+Version: 2.1.1
 Summary: Python interface to MySQL
-Home-page: https://github.com/PyMySQL/mysqlclient-python
+Home-page: https://github.com/PyMySQL/mysqlclient
 Author: Inada Naoki
 Author-email: songofacandy@gmail.com
 License: GPL
-Description: # mysqlclient
-        
-        [![Build Status](https://secure.travis-ci.org/PyMySQL/mysqlclient-python.png)](http://travis-ci.org/PyMySQL/mysqlclient-python)
-        
-        This is a fork of [MySQLdb1](https://github.com/farcepest/MySQLdb1).
-        
-        This project adds Python 3 support and bug fixes.
-        I hope this fork is merged back to MySQLdb1 like distribute was merged back to setuptools.
-        
-        ## Install
-        
-        ### Prerequisites
-        
-        You may need to install the Python and MySQL development headers and libraries like so:
-        
-        * `sudo apt-get install python-dev default-libmysqlclient-dev`  # Debian / Ubuntu
-        * `sudo yum install python-devel mysql-devel`  # Red Hat / CentOS
-        * `brew install mysql-client`  # macOS (Homebrew)
-        
-        On Windows, there are binary wheels you can install without MySQLConnector/C or MSVC.
-        
-        #### Note on Python 3 : if you are using python3 then you need to install python3-dev using the following command :
-        
-        `sudo apt-get install python3-dev` # debian / Ubuntu
-        
-        `sudo yum install python3-devel `  # Red Hat / CentOS
-        
-        ### Install from PyPI
-        
-        `pip install mysqlclient`
-        
-        NOTE: Wheels for Windows may be not released with source package. You should pin version
-        in your `requirements.txt` to avoid trying to install newest source package.
-        
-        
-        ### Install from source
-        
-        1. Download source by `git clone` or [zipfile](https://github.com/PyMySQL/mysqlclient-python/archive/master.zip).
-        2. Customize `site.cfg`
-        3. `python setup.py install`
-        
-        ### Documentation
-        
-        Documentation is hosted on [Read The Docs](https://mysqlclient.readthedocs.io/)
-        
-        
 Platform: ALL
 Classifier: Development Status :: 5 - Production/Stable
 Classifier: Environment :: Other Environment
@@ -64,13 +18,115 @@ Classifier: Operating System :: POSIX :: Linux
 Classifier: Operating System :: Unix
 Classifier: Programming Language :: C
 Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 2
-Classifier: Programming Language :: Python :: 2.7
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.5
 Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
 Classifier: Topic :: Database
 Classifier: Topic :: Database :: Database Engines/Servers
+Requires-Python: >=3.5
 Description-Content-Type: text/markdown
+License-File: LICENSE
+
+# mysqlclient
+
+This project is a fork of [MySQLdb1](https://github.com/farcepest/MySQLdb1).
+This project adds Python 3 support and fixed many bugs.
+
+* PyPI: https://pypi.org/project/mysqlclient/
+* GitHub: https://github.com/PyMySQL/mysqlclient
+
+
+## Support
+
+**Do Not use Github Issue Tracker to ask help.  OSS Maintainer is not free tech support**
+
+When your question looks relating to Python rather than MySQL:
+
+* Python mailing list [python-list](https://mail.python.org/mailman/listinfo/python-list)
+* Slack [pythondev.slack.com](https://pyslackers.com/web/slack)
+
+Or when you have question about MySQL:
+
+* [MySQL Community on Slack](https://lefred.be/mysql-community-on-slack/)
+
+
+## Install
+
+### Windows
+
+Building mysqlclient on Windows is very hard.
+But there are some binary wheels you can install easily.
+
+If binary wheels do not exist for your version of Python, it may be possible to
+build from source, but if this does not work, **do not come asking for support.**
+To build from source, download the
+[MariaDB C Connector](https://mariadb.com/downloads/#connectors) and install
+it. It must be installed in the default location
+(usually "C:\Program Files\MariaDB\MariaDB Connector C" or
+"C:\Program Files (x86)\MariaDB\MariaDB Connector C" for 32-bit). If you
+build the connector yourself or install it in a different location, set the
+environment variable `MYSQLCLIENT_CONNECTOR` before installing. Once you have
+the connector installed and an appropriate version of Visual Studio for your
+version of Python:
+
+```
+$ pip install mysqlclient
+```
+
+### macOS (Homebrew)
+
+Install MySQL and mysqlclient:
+
+```
+# Assume you are activating Python 3 venv
+$ brew install mysql
+$ pip install mysqlclient
+```
+
+If you don't want to install MySQL server, you can use mysql-client instead:
+
+```
+# Assume you are activating Python 3 venv
+$ brew install mysql-client
+$ echo 'export PATH="/usr/local/opt/mysql-client/bin:$PATH"' >> ~/.bash_profile
+$ export PATH="/usr/local/opt/mysql-client/bin:$PATH"
+$ pip install mysqlclient
+```
+
+### Linux
+
+**Note that this is a basic step.  I can not support complete step for build for all
+environment.  If you can see some error, you should fix it by yourself, or ask for
+support in some user forum.  Don't file a issue on the issue tracker.**
+
+You may need to install the Python 3 and MySQL development headers and libraries like so:
+
+* `$ sudo apt-get install python3-dev default-libmysqlclient-dev build-essential`  # Debian / Ubuntu
+* `% sudo yum install python3-devel mysql-devel`  # Red Hat / CentOS
+
+Then you can install mysqlclient via pip now:
+
+```
+$ pip install mysqlclient
+```
+
+### Customize build (POSIX)
+
+mysqlclient uses `mysql_config` or `mariadb_config` by default for finding
+compiler/linker flags.
+
+You can use `MYSQLCLIENT_CFLAGS` and `MYSQLCLIENT_LDFLAGS` environment
+variables to customize compiler/linker options.
+
+```
+$ export MYSQLCLIENT_CFLAGS=`pkg-config mysqlclient --cflags`
+$ export MYSQLCLIENT_LDFLAGS=`pkg-config mysqlclient --libs`
+$ pip install mysqlclient
+```
+
+### Documentation
+
+Documentation is hosted on [Read The Docs](https://mysqlclient.readthedocs.io/)
diff --git a/mysqlclient.egg-info/SOURCES.txt b/mysqlclient.egg-info/SOURCES.txt
index 7d81586..63b42f8 100644
--- a/mysqlclient.egg-info/SOURCES.txt
+++ b/mysqlclient.egg-info/SOURCES.txt
@@ -1,5 +1,4 @@
 HISTORY.rst
-INSTALL.rst
 LICENSE
 MANIFEST.in
 README.md
@@ -12,7 +11,6 @@ site.cfg
 MySQLdb/__init__.py
 MySQLdb/_exceptions.py
 MySQLdb/_mysql.c
-MySQLdb/compat.py
 MySQLdb/connections.py
 MySQLdb/converters.py
 MySQLdb/cursors.py
@@ -42,4 +40,5 @@ tests/test_MySQLdb_dbapi20.py
 tests/test_MySQLdb_nonstandard.py
 tests/test_MySQLdb_times.py
 tests/test__mysql.py
+tests/test_connection.py
 tests/test_cursor.py
\ No newline at end of file
diff --git a/setup.py b/setup.py
index d102996..dfa661c 100644
--- a/setup.py
+++ b/setup.py
@@ -1,7 +1,6 @@
 #!/usr/bin/env python
 
 import os
-import io
 
 import setuptools
 
@@ -10,13 +9,14 @@ if os.name == "posix":
 else:  # assume windows
     from setup_windows import get_config
 
-with io.open('README.md', encoding='utf-8') as f:
+with open("README.md", encoding="utf-8") as f:
     readme = f.read()
 
 metadata, options = get_config()
-metadata['ext_modules'] = [
-    setuptools.Extension("MySQLdb._mysql", sources=['MySQLdb/_mysql.c'], **options)
+metadata["ext_modules"] = [
+    setuptools.Extension("MySQLdb._mysql", sources=["MySQLdb/_mysql.c"], **options)
 ]
-metadata['long_description'] = readme
-metadata['long_description_content_type'] = "text/markdown"
+metadata["long_description"] = readme
+metadata["long_description_content_type"] = "text/markdown"
+metadata["python_requires"] = ">=3.5"
 setuptools.setup(**metadata)
diff --git a/setup_common.py b/setup_common.py
index 03c39bb..5b6927a 100644
--- a/setup_common.py
+++ b/setup_common.py
@@ -1,37 +1,37 @@
-try:
-    # Python 2.x
-    from ConfigParser import SafeConfigParser
-except ImportError:
-    # Python 3.x
-    from configparser import ConfigParser as SafeConfigParser
+from configparser import ConfigParser as SafeConfigParser
+
 
 def get_metadata_and_options():
     config = SafeConfigParser()
-    config.read(['metadata.cfg', 'site.cfg'])
+    config.read(["metadata.cfg", "site.cfg"])
 
-    metadata = dict(config.items('metadata'))
-    options = dict(config.items('options'))
+    metadata = dict(config.items("metadata"))
+    options = dict(config.items("options"))
 
-    metadata['py_modules'] = list(filter(None, metadata['py_modules'].split('\n')))
-    metadata['classifiers'] = list(filter(None, metadata['classifiers'].split('\n')))
+    metadata["py_modules"] = list(filter(None, metadata["py_modules"].split("\n")))
+    metadata["classifiers"] = list(filter(None, metadata["classifiers"].split("\n")))
 
     return metadata, options
 
+
 def enabled(options, option):
     value = options[option]
     s = value.lower()
-    if s in ('yes','true','1','y'):
+    if s in ("yes", "true", "1", "y"):
         return True
-    elif s in ('no', 'false', '0', 'n'):
+    elif s in ("no", "false", "0", "n"):
         return False
     else:
-        raise ValueError("Unknown value %s for option %s" % (value, option))
+        raise ValueError("Unknown value {} for option {}".format(value, option))
+
 
 def create_release_file(metadata):
-    rel = open("MySQLdb/release.py",'w')
-    rel.write("""
+    with open("MySQLdb/release.py", "w", encoding="utf-8") as rel:
+        rel.write(
+            """
 __author__ = "%(author)s <%(author_email)s>"
 version_info = %(version_info)s
 __version__ = "%(version)s"
-""" % metadata)
-    rel.close()
+"""
+            % metadata
+        )
diff --git a/setup_posix.py b/setup_posix.py
index c65c045..99763cb 100644
--- a/setup_posix.py
+++ b/setup_posix.py
@@ -1,123 +1,168 @@
-import os, sys
-try:
-    from ConfigParser import SafeConfigParser
-except ImportError:
-    from configparser import ConfigParser as SafeConfigParser
+import os
+import sys
 
 # This dequote() business is required for some older versions
 # of mysql_config
 
+
 def dequote(s):
     if not s:
-        raise Exception("Wrong MySQL configuration: maybe https://bugs.mysql.com/bug.php?id=86971 ?")
+        raise Exception(
+            "Wrong MySQL configuration: maybe https://bugs.mysql.com/bug.php?id=86971 ?"
+        )
     if s[0] in "\"'" and s[0] == s[-1]:
         s = s[1:-1]
     return s
 
+
 _mysql_config_path = "mysql_config"
 
-def mysql_config(what):
-    from os import popen
 
-    f = popen("%s --%s" % (_mysql_config_path, what))
+def mysql_config(what):
+    cmd = "{} --{}".format(_mysql_config_path, what)
+    print(cmd)
+    f = os.popen(cmd)
     data = f.read().strip().split()
     ret = f.close()
     if ret:
-        if ret/256:
+        if ret / 256:
             data = []
-        if ret/256 > 1:
-            raise EnvironmentError("%s not found" % (_mysql_config_path,))
+        if ret / 256 > 1:
+            raise OSError("{} not found".format(_mysql_config_path))
+    print(data)
     return data
 
+
 def get_config():
     from setup_common import get_metadata_and_options, enabled, create_release_file
+
     global _mysql_config_path
 
     metadata, options = get_metadata_and_options()
 
-    if 'mysql_config' in options:
-        _mysql_config_path = options['mysql_config']
+    if "mysql_config" in options:
+        _mysql_config_path = options["mysql_config"]
     else:
         try:
-            mysql_config('version')
-        except EnvironmentError:
+            mysql_config("version")
+        except OSError:
             # try mariadb_config
             _mysql_config_path = "mariadb_config"
             try:
-                mysql_config('version')
-            except EnvironmentError:
+                mysql_config("version")
+            except OSError:
                 _mysql_config_path = "mysql_config"
 
     extra_objects = []
-    static = enabled(options, 'static')
+    static = enabled(options, "static")
 
     # allow a command-line option to override the base config file to permit
     # a static build to be created via requirements.txt
     #
-    if '--static' in sys.argv:
+    if "--static" in sys.argv:
         static = True
-        sys.argv.remove('--static')
+        sys.argv.remove("--static")
+
+    libs = os.environ.get("MYSQLCLIENT_LDFLAGS")
+    if libs:
+        libs = libs.strip().split()
+    else:
+        libs = mysql_config("libs")
+    library_dirs = [dequote(i[2:]) for i in libs if i.startswith("-L")]
+    libraries = [dequote(i[2:]) for i in libs if i.startswith("-l")]
+    extra_link_args = [x for x in libs if not x.startswith(("-l", "-L"))]
+
+    cflags = os.environ.get("MYSQLCLIENT_CFLAGS")
+    if cflags:
+        use_mysqlconfig_cflags = False
+        cflags = cflags.strip().split()
+    else:
+        use_mysqlconfig_cflags = True
+        cflags = mysql_config("cflags")
 
-    libs = mysql_config("libs")
-    library_dirs = [dequote(i[2:]) for i in libs if i.startswith('-L')]
-    libraries = [dequote(i[2:]) for i in libs if i.startswith('-l')]
-    extra_link_args = [x for x in libs if not x.startswith(('-l', '-L'))]
+    include_dirs = []
+    extra_compile_args = ["-std=c99"]
 
-    removable_compile_args = ('-I', '-L', '-l')
-    extra_compile_args = [i.replace("%", "%%") for i in mysql_config("cflags")
-                          if i[:2] not in removable_compile_args]
+    for a in cflags:
+        if a.startswith("-I"):
+            include_dirs.append(dequote(a[2:]))
+        elif a.startswith(("-L", "-l")):  # This should be LIBS.
+            pass
+        else:
+            extra_compile_args.append(a.replace("%", "%%"))
 
     # Copy the arch flags for linking as well
-    for i in range(len(extra_compile_args)):
-        if extra_compile_args[i] == '-arch':
-            extra_link_args += ['-arch', extra_compile_args[i + 1]]
-
-    include_dirs = [dequote(i[2:])
-                    for i in mysql_config('include') if i.startswith('-I')]
+    try:
+        i = extra_compile_args.index("-arch")
+        if "-arch" not in extra_link_args:
+            extra_link_args += ["-arch", extra_compile_args[i + 1]]
+    except ValueError:
+        pass
 
     if static:
         # properly handle mysql client libraries that are not called libmysqlclient
         client = None
-        CLIENT_LIST = ['mysqlclient', 'mysqlclient_r', 'mysqld', 'mariadb',
-                       'mariadbclient', 'perconaserverclient', 'perconaserverclient_r']
+        CLIENT_LIST = [
+            "mysqlclient",
+            "mysqlclient_r",
+            "mysqld",
+            "mariadb",
+            "mariadbclient",
+            "perconaserverclient",
+            "perconaserverclient_r",
+        ]
         for c in CLIENT_LIST:
             if c in libraries:
                 client = c
                 break
 
-        if client == 'mariadb':
-            client = 'mariadbclient'
+        if client == "mariadb":
+            client = "mariadbclient"
         if client is None:
             raise ValueError("Couldn't identify mysql client library")
 
-        extra_objects.append(os.path.join(library_dirs[0], 'lib%s.a' % client))
+        extra_objects.append(os.path.join(library_dirs[0], "lib%s.a" % client))
         if client in libraries:
             libraries.remove(client)
+    else:
+        if use_mysqlconfig_cflags:
+            # mysql_config may have "-lmysqlclient -lz -lssl -lcrypto", but zlib and
+            # ssl is not used by _mysql.  They are needed only for static build.
+            for L in ("crypto", "ssl", "z", "zstd"):
+                if L in libraries:
+                    libraries.remove(L)
 
     name = "mysqlclient"
-    metadata['name'] = name
+    metadata["name"] = name
 
     define_macros = [
-        ('version_info', metadata['version_info']),
-        ('__version__', metadata['version']),
-        ]
+        ("version_info", metadata["version_info"]),
+        ("__version__", metadata["version"]),
+    ]
     create_release_file(metadata)
-    del metadata['version_info']
+    del metadata["version_info"]
     ext_options = dict(
-        library_dirs = library_dirs,
-        libraries = libraries,
-        extra_compile_args = extra_compile_args,
-        extra_link_args = extra_link_args,
-        include_dirs = include_dirs,
-        extra_objects = extra_objects,
-        define_macros = define_macros,
+        library_dirs=library_dirs,
+        libraries=libraries,
+        extra_compile_args=extra_compile_args,
+        extra_link_args=extra_link_args,
+        include_dirs=include_dirs,
+        extra_objects=extra_objects,
+        define_macros=define_macros,
     )
 
     # newer versions of gcc require libstdc++ if doing a static build
     if static:
-        ext_options['language'] = 'c++'
+        ext_options["language"] = "c++"
+
+    print("ext_options:")
+    for k, v in ext_options.items():
+        print("  {}: {}".format(k, v))
 
     return metadata, ext_options
 
+
 if __name__ == "__main__":
-    sys.stderr.write("""You shouldn't be running this directly; it is used by setup.py.""")
+    sys.stderr.write(
+        """You shouldn't be running this directly; it is used by setup.py."""
+    )
diff --git a/setup_windows.py b/setup_windows.py
index cb2cbab..b2feb7d 100644
--- a/setup_windows.py
+++ b/setup_windows.py
@@ -1,52 +1,64 @@
-import os, sys
-from distutils.msvccompiler import get_build_version
+import os
+import sys
 
 
 def get_config():
-    from setup_common import get_metadata_and_options, enabled, create_release_file
+    from setup_common import get_metadata_and_options, create_release_file
 
     metadata, options = get_metadata_and_options()
 
-    connector = options["connector"]
+    client = "mariadbclient"
+    connector = os.environ.get("MYSQLCLIENT_CONNECTOR", options.get("connector"))
+    if not connector:
+        connector = os.path.join(
+            os.environ["ProgramFiles"], "MariaDB", "MariaDB Connector C"
+        )
 
     extra_objects = []
 
-    # client = "mysqlclient"
-    client = "mariadbclient"
-
-    vcversion = int(get_build_version())
-    if client == "mariadbclient":
-        library_dirs = [os.path.join(connector, 'lib', 'mariadb')]
-        libraries = ['kernel32', 'advapi32', 'wsock32', 'shlwapi', 'Ws2_32', client ]
-        include_dirs = [os.path.join(connector, 'include', 'mariadb')]
-    else:
-        library_dirs = [os.path.join(connector, r'lib\vs%d' % vcversion),
-                        os.path.join(connector, "lib")]
-        libraries = ['kernel32', 'advapi32', 'wsock32', client ]
-        include_dirs = [os.path.join(connector, r'include')]
+    library_dirs = [
+        os.path.join(connector, "lib", "mariadb"),
+        os.path.join(connector, "lib"),
+    ]
+    libraries = [
+        "kernel32",
+        "advapi32",
+        "wsock32",
+        "shlwapi",
+        "Ws2_32",
+        "crypt32",
+        "secur32",
+        "bcrypt",
+        client,
+    ]
+    include_dirs = [
+        os.path.join(connector, "include", "mariadb"),
+        os.path.join(connector, "include"),
+    ]
 
-    extra_compile_args = ['/Zl', '/D_CRT_SECURE_NO_WARNINGS' ]
-    extra_link_args = ['/MANIFEST']
+    extra_link_args = ["/MANIFEST"]
 
     name = "mysqlclient"
-    metadata['name'] = name
+    metadata["name"] = name
 
     define_macros = [
-        ('version_info', metadata['version_info']),
-        ('__version__', metadata['version']),
-        ]
+        ("version_info", metadata["version_info"]),
+        ("__version__", metadata["version"]),
+    ]
     create_release_file(metadata)
-    del metadata['version_info']
+    del metadata["version_info"]
     ext_options = dict(
-        library_dirs = library_dirs,
-        libraries = libraries,
-        extra_compile_args = extra_compile_args,
-        extra_link_args = extra_link_args,
-        include_dirs = include_dirs,
-        extra_objects = extra_objects,
-        define_macros = define_macros,
+        library_dirs=library_dirs,
+        libraries=libraries,
+        extra_link_args=extra_link_args,
+        include_dirs=include_dirs,
+        extra_objects=extra_objects,
+        define_macros=define_macros,
     )
     return metadata, ext_options
 
+
 if __name__ == "__main__":
-    sys.stderr.write("""You shouldn't be running this directly; it is used by setup.py.""")
+    sys.stderr.write(
+        """You shouldn't be running this directly; it is used by setup.py."""
+    )
diff --git a/site.cfg b/site.cfg
index 6b4596a..08a14b0 100644
--- a/site.cfg
+++ b/site.cfg
@@ -9,4 +9,4 @@ static = False
 
 # http://stackoverflow.com/questions/1972259/mysql-python-install-problem-using-virtualenv-windows-pip
 # Windows connector libs for MySQL. You need a 32-bit connector for your 32-bit Python build.
-connector = C:\Program Files (x86)\MySQL\MySQL Connector C 6.1
+connector =
diff --git a/tests/capabilities.py b/tests/capabilities.py
index 5d91379..da753d1 100644
--- a/tests/capabilities.py
+++ b/tests/capabilities.py
@@ -6,47 +6,51 @@
 
 """
 from time import time
-import array
 import unittest
 from configdb import connection_factory
 
-from MySQLdb.compat import unichr
-
 
 class DatabaseTest(unittest.TestCase):
 
     db_module = None
     connect_args = ()
     connect_kwargs = dict()
-    create_table_extra = ''
+    create_table_extra = ""
     rows = 10
     debug = False
 
     def setUp(self):
-        import gc
+
         db = connection_factory(**self.connect_kwargs)
         self.connection = db
         self.cursor = db.cursor()
-        self.BLOBUText = u''.join([unichr(i) for i in range(16384)])
-        self.BLOBBinary = self.db_module.Binary((u''.join([unichr(i) for i in range(256)] * 16)).encode('latin1'))
+        self.BLOBUText = "".join([chr(i) for i in range(16384)])
+        self.BLOBBinary = self.db_module.Binary(
+            ("".join([chr(i) for i in range(256)] * 16)).encode("latin1")
+        )
 
     leak_test = True
 
     def tearDown(self):
         if self.leak_test:
             import gc
+
             del self.cursor
             orphans = gc.collect()
-            self.failIf(orphans, "%d orphaned objects found after deleting cursor" % orphans)
+            self.failIf(
+                orphans, "%d orphaned objects found after deleting cursor" % orphans
+            )
 
             del self.connection
             orphans = gc.collect()
-            self.failIf(orphans, "%d orphaned objects found after deleting connection" % orphans)
+            self.failIf(
+                orphans, "%d orphaned objects found after deleting connection" % orphans
+            )
 
     def table_exists(self, name):
         try:
-            self.cursor.execute('select * from %s where 1=0' % name)
-        except:
+            self.cursor.execute("select * from %s where 1=0" % name)
+        except Exception:
             return False
         else:
             return True
@@ -57,98 +61,111 @@ class DatabaseTest(unittest.TestCase):
     def new_table_name(self):
         i = id(self.cursor)
         while True:
-            name = self.quote_identifier('tb%08x' % i)
+            name = self.quote_identifier("tb%08x" % i)
             if not self.table_exists(name):
                 return name
             i = i + 1
 
     def create_table(self, columndefs):
 
-        """ Create a table using a list of column definitions given in
-            columndefs.
+        """Create a table using a list of column definitions given in
+        columndefs.
 
-            generator must be a function taking arguments (row_number,
-            col_number) returning a suitable data object for insertion
-            into the table.
+        generator must be a function taking arguments (row_number,
+        col_number) returning a suitable data object for insertion
+        into the table.
 
         """
         self.table = self.new_table_name()
-        self.cursor.execute('CREATE TABLE %s (%s) %s' %
-                            (self.table,
-                             ',\n'.join(columndefs),
-                             self.create_table_extra))
+        self.cursor.execute(
+            "CREATE TABLE %s (%s) %s"
+            % (self.table, ",\n".join(columndefs), self.create_table_extra)
+        )
 
     def check_data_integrity(self, columndefs, generator):
         # insert
         self.create_table(columndefs)
-        insert_statement = ('INSERT INTO %s VALUES (%s)' %
-                            (self.table,
-                             ','.join(['%s'] * len(columndefs))))
-        data = [ [ generator(i,j) for j in range(len(columndefs)) ]
-                 for i in range(self.rows) ]
+        insert_statement = "INSERT INTO %s VALUES (%s)" % (
+            self.table,
+            ",".join(["%s"] * len(columndefs)),
+        )
+        data = [
+            [generator(i, j) for j in range(len(columndefs))] for i in range(self.rows)
+        ]
         self.cursor.executemany(insert_statement, data)
         self.connection.commit()
         # verify
-        self.cursor.execute('select * from %s' % self.table)
-        l = self.cursor.fetchall()
-        self.assertEqual(len(l), self.rows)
+        self.cursor.execute("select * from %s" % self.table)
+        res = self.cursor.fetchall()
+        self.assertEqual(len(res), self.rows)
         try:
             for i in range(self.rows):
                 for j in range(len(columndefs)):
-                    self.assertEqual(l[i][j], generator(i,j))
+                    self.assertEqual(res[i][j], generator(i, j))
         finally:
             if not self.debug:
-                self.cursor.execute('drop table %s' % (self.table))
+                self.cursor.execute("drop table %s" % (self.table))
 
     def test_transactions(self):
-        columndefs = ( 'col1 INT', 'col2 VARCHAR(255)')
+        columndefs = ("col1 INT", "col2 VARCHAR(255)")
+
         def generator(row, col):
-            if col == 0: return row
-            else: return ('%i' % (row%10))*255
+            if col == 0:
+                return row
+            else:
+                return ("%i" % (row % 10)) * 255
+
         self.create_table(columndefs)
-        insert_statement = ('INSERT INTO %s VALUES (%s)' %
-                            (self.table,
-                             ','.join(['%s'] * len(columndefs))))
-        data = [ [ generator(i,j) for j in range(len(columndefs)) ]
-                 for i in range(self.rows) ]
+        insert_statement = "INSERT INTO %s VALUES (%s)" % (
+            self.table,
+            ",".join(["%s"] * len(columndefs)),
+        )
+        data = [
+            [generator(i, j) for j in range(len(columndefs))] for i in range(self.rows)
+        ]
         self.cursor.executemany(insert_statement, data)
         # verify
         self.connection.commit()
-        self.cursor.execute('select * from %s' % self.table)
-        l = self.cursor.fetchall()
-        self.assertEqual(len(l), self.rows)
+        self.cursor.execute("select * from %s" % self.table)
+        res = self.cursor.fetchall()
+        self.assertEqual(len(res), self.rows)
         for i in range(self.rows):
             for j in range(len(columndefs)):
-                self.assertEqual(l[i][j], generator(i,j))
-        delete_statement = 'delete from %s where col1=%%s' % self.table
+                self.assertEqual(res[i][j], generator(i, j))
+        delete_statement = "delete from %s where col1=%%s" % self.table
         self.cursor.execute(delete_statement, (0,))
-        self.cursor.execute('select col1 from %s where col1=%s' % \
-                            (self.table, 0))
-        l = self.cursor.fetchall()
-        self.assertFalse(l, "DELETE didn't work")
+        self.cursor.execute("select col1 from %s where col1=%s" % (self.table, 0))
+        res = self.cursor.fetchall()
+        self.assertFalse(res, "DELETE didn't work")
         self.connection.rollback()
-        self.cursor.execute('select col1 from %s where col1=%s' % \
-                            (self.table, 0))
-        l = self.cursor.fetchall()
-        self.assertTrue(len(l) == 1, "ROLLBACK didn't work")
-        self.cursor.execute('drop table %s' % (self.table))
+        self.cursor.execute("select col1 from %s where col1=%s" % (self.table, 0))
+        res = self.cursor.fetchall()
+        self.assertTrue(len(res) == 1, "ROLLBACK didn't work")
+        self.cursor.execute("drop table %s" % (self.table))
 
     def test_truncation(self):
-        columndefs = ( 'col1 INT', 'col2 VARCHAR(255)')
+        columndefs = ("col1 INT", "col2 VARCHAR(255)")
+
         def generator(row, col):
-            if col == 0: return row
-            else: return ('%i' % (row%10))*((255-self.rows//2)+row)
+            if col == 0:
+                return row
+            else:
+                return ("%i" % (row % 10)) * ((255 - self.rows // 2) + row)
+
         self.create_table(columndefs)
-        insert_statement = ('INSERT INTO %s VALUES (%s)' %
-                            (self.table,
-                             ','.join(['%s'] * len(columndefs))))
+        insert_statement = "INSERT INTO %s VALUES (%s)" % (
+            self.table,
+            ",".join(["%s"] * len(columndefs)),
+        )
 
         try:
-            self.cursor.execute(insert_statement, (0, '0'*256))
+            self.cursor.execute(insert_statement, (0, "0" * 256))
         except self.connection.DataError:
             pass
         else:
-            self.fail("Over-long column did not generate warnings/exception with single insert")
+            self.fail(
+                "Over-long column did not generate warnings/exception with single insert"  # noqa: E501
+            )
 
         self.connection.rollback()
 
@@ -156,143 +173,145 @@ class DatabaseTest(unittest.TestCase):
             for i in range(self.rows):
                 data = []
                 for j in range(len(columndefs)):
-                    data.append(generator(i,j))
-                self.cursor.execute(insert_statement,tuple(data))
+                    data.append(generator(i, j))
+                self.cursor.execute(insert_statement, tuple(data))
         except self.connection.DataError:
             pass
         else:
-            self.fail("Over-long columns did not generate warnings/exception with execute()")
+            self.fail(
+                "Over-long columns did not generate warnings/exception with execute()"  # noqa: E501
+            )
 
         self.connection.rollback()
 
         try:
-            data = [ [ generator(i,j) for j in range(len(columndefs)) ]
-                     for i in range(self.rows) ]
+            data = [
+                [generator(i, j) for j in range(len(columndefs))]
+                for i in range(self.rows)
+            ]
             self.cursor.executemany(insert_statement, data)
         except self.connection.DataError:
             pass
         else:
-            self.fail("Over-long columns did not generate warnings/exception with executemany()")
+            self.fail(
+                "Over-long columns did not generate warnings/exception with executemany()"  # noqa: E501
+            )
 
         self.connection.rollback()
-        self.cursor.execute('drop table %s' % (self.table))
+        self.cursor.execute("drop table %s" % (self.table))
 
     def test_CHAR(self):
         # Character data
-        def generator(row,col):
-            return ('%i' % ((row+col) % 10)) * 255
-        self.check_data_integrity(
-            ('col1 char(255)','col2 char(255)'),
-            generator)
+        def generator(row, col):
+            return ("%i" % ((row + col) % 10)) * 255
+
+        self.check_data_integrity(("col1 char(255)", "col2 char(255)"), generator)
 
     def test_INT(self):
         # Number data
-        def generator(row,col):
-            return row*row
-        self.check_data_integrity(
-            ('col1 INT',),
-            generator)
+        def generator(row, col):
+            return row * row
+
+        self.check_data_integrity(("col1 INT",), generator)
 
     def test_DECIMAL(self):
         # DECIMAL
         from decimal import Decimal
-        def generator(row,col):
+
+        def generator(row, col):
             return Decimal("%d.%02d" % (row, col))
-        self.check_data_integrity(
-            ('col1 DECIMAL(5,2)',),
-            generator)
 
-        val = Decimal('1.11111111111111119E-7')
-        self.cursor.execute('SELECT %s', (val,))
+        self.check_data_integrity(("col1 DECIMAL(5,2)",), generator)
+
+        val = Decimal("1.11111111111111119E-7")
+        self.cursor.execute("SELECT %s", (val,))
         result = self.cursor.fetchone()[0]
         self.assertEqual(result, val)
         self.assertIsInstance(result, Decimal)
 
-        self.cursor.execute('SELECT %s + %s', (Decimal('0.1'), Decimal('0.2')))
+        self.cursor.execute("SELECT %s + %s", (Decimal("0.1"), Decimal("0.2")))
         result = self.cursor.fetchone()[0]
-        self.assertEqual(result, Decimal('0.3'))
+        self.assertEqual(result, Decimal("0.3"))
         self.assertIsInstance(result, Decimal)
 
     def test_DATE(self):
         ticks = time()
-        def generator(row,col):
-            return self.db_module.DateFromTicks(ticks+row*86400-col*1313)
-        self.check_data_integrity(
-                 ('col1 DATE',),
-                 generator)
+
+        def generator(row, col):
+            return self.db_module.DateFromTicks(ticks + row * 86400 - col * 1313)
+
+        self.check_data_integrity(("col1 DATE",), generator)
 
     def test_TIME(self):
         ticks = time()
-        def generator(row,col):
-            return self.db_module.TimeFromTicks(ticks+row*86400-col*1313)
-        self.check_data_integrity(
-                 ('col1 TIME',),
-                 generator)
+
+        def generator(row, col):
+            return self.db_module.TimeFromTicks(ticks + row * 86400 - col * 1313)
+
+        self.check_data_integrity(("col1 TIME",), generator)
 
     def test_DATETIME(self):
         ticks = time()
-        def generator(row,col):
-            return self.db_module.TimestampFromTicks(ticks+row*86400-col*1313)
-        self.check_data_integrity(
-                 ('col1 DATETIME',),
-                 generator)
+
+        def generator(row, col):
+            return self.db_module.TimestampFromTicks(ticks + row * 86400 - col * 1313)
+
+        self.check_data_integrity(("col1 DATETIME",), generator)
 
     def test_TIMESTAMP(self):
         ticks = time()
-        def generator(row,col):
-            return self.db_module.TimestampFromTicks(ticks+row*86400-col*1313)
-        self.check_data_integrity(
-                 ('col1 TIMESTAMP',),
-                 generator)
+
+        def generator(row, col):
+            return self.db_module.TimestampFromTicks(ticks + row * 86400 - col * 1313)
+
+        self.check_data_integrity(("col1 TIMESTAMP",), generator)
 
     def test_fractional_TIMESTAMP(self):
         ticks = time()
-        def generator(row,col):
-            return self.db_module.TimestampFromTicks(ticks+row*86400-col*1313+row*0.7*col/3.0)
-        self.check_data_integrity(
-                 ('col1 TIMESTAMP',),
-                 generator)
+
+        def generator(row, col):
+            return self.db_module.TimestampFromTicks(
+                ticks + row * 86400 - col * 1313 + row * 0.7 * col / 3.0
+            )
+
+        self.check_data_integrity(("col1 TIMESTAMP",), generator)
 
     def test_LONG(self):
-        def generator(row,col):
+        def generator(row, col):
             if col == 0:
                 return row
             else:
-                return self.BLOBUText # 'BLOB Text ' * 1024
-        self.check_data_integrity(
-                 ('col1 INT','col2 LONG'),
-                 generator)
+                return self.BLOBUText  # 'BLOB Text ' * 1024
+
+        self.check_data_integrity(("col1 INT", "col2 LONG"), generator)
 
     def test_TEXT(self):
-        def generator(row,col):
-            return self.BLOBUText # 'BLOB Text ' * 1024
-        self.check_data_integrity(
-                 ('col2 TEXT',),
-                 generator)
+        def generator(row, col):
+            return self.BLOBUText  # 'BLOB Text ' * 1024
+
+        self.check_data_integrity(("col2 TEXT",), generator)
 
     def test_LONG_BYTE(self):
-        def generator(row,col):
+        def generator(row, col):
             if col == 0:
                 return row
             else:
-                return self.BLOBBinary # 'BLOB\000Binary ' * 1024
-        self.check_data_integrity(
-                 ('col1 INT','col2 LONG BYTE'),
-                 generator)
+                return self.BLOBBinary  # 'BLOB\000Binary ' * 1024
+
+        self.check_data_integrity(("col1 INT", "col2 LONG BYTE"), generator)
 
     def test_BLOB(self):
-        def generator(row,col):
+        def generator(row, col):
             if col == 0:
                 return row
             else:
-                return self.BLOBBinary # 'BLOB\000Binary ' * 1024
-        self.check_data_integrity(
-                 ('col1 INT','col2 BLOB'),
-                 generator)
+                return self.BLOBBinary  # 'BLOB\000Binary ' * 1024
+
+        self.check_data_integrity(("col1 INT", "col2 BLOB"), generator)
 
     def test_DOUBLE(self):
         for val in (18014398509481982.0, 0.1):
-            self.cursor.execute('SELECT %s', (val,));
+            self.cursor.execute("SELECT %s", (val,))
             result = self.cursor.fetchone()[0]
             self.assertEqual(result, val)
             self.assertIsInstance(result, float)
diff --git a/tests/configdb.py b/tests/configdb.py
index 307cc3f..c294903 100644
--- a/tests/configdb.py
+++ b/tests/configdb.py
@@ -3,11 +3,11 @@
 from os import environ, path
 
 tests_path = path.dirname(__file__)
-conf_file = environ.get('TESTDB', 'default.cnf')
+conf_file = environ.get("TESTDB", "default.cnf")
 conf_path = path.join(tests_path, conf_file)
 connect_kwargs = dict(
-    read_default_file = conf_path,
-    read_default_group = "MySQLdb-tests",
+    read_default_file=conf_path,
+    read_default_group="MySQLdb-tests",
 )
 
 
@@ -19,6 +19,7 @@ def connection_kwargs(kwargs):
 
 def connection_factory(**kwargs):
     import MySQLdb
+
     db_kwargs = connection_kwargs(kwargs)
     db = MySQLdb.connect(**db_kwargs)
     return db
diff --git a/tests/dbapi20.py b/tests/dbapi20.py
index 79c188a..4965c9b 100644
--- a/tests/dbapi20.py
+++ b/tests/dbapi20.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-''' Python DB API 2.0 driver compliance unit test suite.
+""" Python DB API 2.0 driver compliance unit test suite.
 
     This software is Public Domain and may be used without restrictions.
 
@@ -9,11 +9,11 @@
   this is turning out to be a thoroughly unwholesome unit test."
 
     -- Ian Bicking
-'''
+"""
 
-__rcs_id__  = '$Id$'
-__version__ = '$Revision$'[11:-2]
-__author__ = 'Stuart Bishop <zen@shangri-la.dropbear.id.au>'
+__rcs_id__ = "$Id$"
+__version__ = "$Revision$"[11:-2]
+__author__ = "Stuart Bishop <zen@shangri-la.dropbear.id.au>"
 
 import unittest
 import time
@@ -64,65 +64,66 @@ import time
 # - Fix bugs in test_setoutputsize_basic and test_setinputsizes
 #
 
+
 class DatabaseAPI20Test(unittest.TestCase):
-    ''' Test a database self.driver for DB API 2.0 compatibility.
-        This implementation tests Gadfly, but the TestCase
-        is structured so that other self.drivers can subclass this
-        test case to ensure compiliance with the DB-API. It is
-        expected that this TestCase may be expanded in the future
-        if ambiguities or edge conditions are discovered.
+    """Test a database self.driver for DB API 2.0 compatibility.
+    This implementation tests Gadfly, but the TestCase
+    is structured so that other self.drivers can subclass this
+    test case to ensure compiliance with the DB-API. It is
+    expected that this TestCase may be expanded in the future
+    if ambiguities or edge conditions are discovered.
 
-        The 'Optional Extensions' are not yet being tested.
+    The 'Optional Extensions' are not yet being tested.
 
-        self.drivers should subclass this test, overriding setUp, tearDown,
-        self.driver, connect_args and connect_kw_args. Class specification
-        should be as follows:
+    self.drivers should subclass this test, overriding setUp, tearDown,
+    self.driver, connect_args and connect_kw_args. Class specification
+    should be as follows:
 
-        import dbapi20
-        class mytest(dbapi20.DatabaseAPI20Test):
-           [...]
+    import dbapi20
+    class mytest(dbapi20.DatabaseAPI20Test):
+       [...]
 
-        Don't 'import DatabaseAPI20Test from dbapi20', or you will
-        confuse the unit tester - just 'import dbapi20'.
-    '''
+    Don't 'import DatabaseAPI20Test from dbapi20', or you will
+    confuse the unit tester - just 'import dbapi20'.
+    """
 
     # The self.driver module. This should be the module where the 'connect'
     # method is to be found
     driver = None
-    connect_args = () # List of arguments to pass to connect
-    connect_kw_args = {} # Keyword arguments for connect
-    table_prefix = 'dbapi20test_' # If you need to specify a prefix for tables
+    connect_args = ()  # List of arguments to pass to connect
+    connect_kw_args = {}  # Keyword arguments for connect
+    table_prefix = "dbapi20test_"  # If you need to specify a prefix for tables
 
-    ddl1 = 'create table %sbooze (name varchar(20))' % table_prefix
-    ddl2 = 'create table %sbarflys (name varchar(20))' % table_prefix
-    xddl1 = 'drop table %sbooze' % table_prefix
-    xddl2 = 'drop table %sbarflys' % table_prefix
+    ddl1 = "create table %sbooze (name varchar(20))" % table_prefix
+    ddl2 = "create table %sbarflys (name varchar(20))" % table_prefix
+    xddl1 = "drop table %sbooze" % table_prefix
+    xddl2 = "drop table %sbarflys" % table_prefix
 
-    lowerfunc = 'lower' # Name of stored procedure to convert string->lowercase
+    lowerfunc = "lower"  # Name of stored procedure to convert string->lowercase
 
     # Some drivers may need to override these helpers, for example adding
     # a 'commit' after the execute.
-    def executeDDL1(self,cursor):
+    def executeDDL1(self, cursor):
         cursor.execute(self.ddl1)
 
-    def executeDDL2(self,cursor):
+    def executeDDL2(self, cursor):
         cursor.execute(self.ddl2)
 
     def setUp(self):
-        ''' self.drivers should override this method to perform required setup
-            if any is necessary, such as creating the database.
-        '''
+        """self.drivers should override this method to perform required setup
+        if any is necessary, such as creating the database.
+        """
         pass
 
     def tearDown(self):
-        ''' self.drivers should override this method to perform required cleanup
-            if any is necessary, such as deleting the test database.
-            The default drops the tables that may be created.
-        '''
+        """self.drivers should override this method to perform required cleanup
+        if any is necessary, such as deleting the test database.
+        The default drops the tables that may be created.
+        """
         con = self._connect()
         try:
             cur = con.cursor()
-            for ddl in (self.xddl1,self.xddl2):
+            for ddl in (self.xddl1, self.xddl2):
                 try:
                     cur.execute(ddl)
                     con.commit()
@@ -135,9 +136,7 @@ class DatabaseAPI20Test(unittest.TestCase):
 
     def _connect(self):
         try:
-            return self.driver.connect(
-                *self.connect_args,**self.connect_kw_args
-                )
+            return self.driver.connect(*self.connect_args, **self.connect_kw_args)
         except AttributeError:
             self.fail("No connect method found in self.driver module")
 
@@ -150,7 +149,7 @@ class DatabaseAPI20Test(unittest.TestCase):
             # Must exist
             apilevel = self.driver.apilevel
             # Must equal 2.0
-            self.assertEqual(apilevel,'2.0')
+            self.assertEqual(apilevel, "2.0")
         except AttributeError:
             self.fail("Driver doesn't define apilevel")
 
@@ -159,7 +158,7 @@ class DatabaseAPI20Test(unittest.TestCase):
             # Must exist
             threadsafety = self.driver.threadsafety
             # Must be a valid value
-            self.assertTrue(threadsafety in (0,1,2,3))
+            self.assertTrue(threadsafety in (0, 1, 2, 3))
         except AttributeError:
             self.fail("Driver doesn't define threadsafety")
 
@@ -168,38 +167,24 @@ class DatabaseAPI20Test(unittest.TestCase):
             # Must exist
             paramstyle = self.driver.paramstyle
             # Must be a valid value
-            self.assertTrue(paramstyle in (
-                'qmark','numeric','named','format','pyformat'
-                ))
+            self.assertTrue(
+                paramstyle in ("qmark", "numeric", "named", "format", "pyformat")
+            )
         except AttributeError:
             self.fail("Driver doesn't define paramstyle")
 
     def test_Exceptions(self):
         # Make sure required exceptions exist, and are in the
         # defined hierarchy.
-        self.assertTrue(issubclass(self.driver.Warning,Exception))
-        self.assertTrue(issubclass(self.driver.Error,Exception))
-        self.assertTrue(
-            issubclass(self.driver.InterfaceError,self.driver.Error)
-            )
-        self.assertTrue(
-            issubclass(self.driver.DatabaseError,self.driver.Error)
-            )
-        self.assertTrue(
-            issubclass(self.driver.OperationalError,self.driver.Error)
-            )
-        self.assertTrue(
-            issubclass(self.driver.IntegrityError,self.driver.Error)
-            )
-        self.assertTrue(
-            issubclass(self.driver.InternalError,self.driver.Error)
-            )
-        self.assertTrue(
-            issubclass(self.driver.ProgrammingError,self.driver.Error)
-            )
-        self.assertTrue(
-            issubclass(self.driver.NotSupportedError,self.driver.Error)
-            )
+        self.assertTrue(issubclass(self.driver.Warning, Exception))
+        self.assertTrue(issubclass(self.driver.Error, Exception))
+        self.assertTrue(issubclass(self.driver.InterfaceError, self.driver.Error))
+        self.assertTrue(issubclass(self.driver.DatabaseError, self.driver.Error))
+        self.assertTrue(issubclass(self.driver.OperationalError, self.driver.Error))
+        self.assertTrue(issubclass(self.driver.IntegrityError, self.driver.Error))
+        self.assertTrue(issubclass(self.driver.InternalError, self.driver.Error))
+        self.assertTrue(issubclass(self.driver.ProgrammingError, self.driver.Error))
+        self.assertTrue(issubclass(self.driver.NotSupportedError, self.driver.Error))
 
     def test_ExceptionsAsConnectionAttributes(self):
         # OPTIONAL EXTENSION
@@ -220,7 +205,6 @@ class DatabaseAPI20Test(unittest.TestCase):
         self.assertTrue(con.ProgrammingError is drv.ProgrammingError)
         self.assertTrue(con.NotSupportedError is drv.NotSupportedError)
 
-
     def test_commit(self):
         con = self._connect()
         try:
@@ -233,7 +217,7 @@ class DatabaseAPI20Test(unittest.TestCase):
         con = self._connect()
         # If rollback is defined, it should either work or throw
         # the documented exception
-        if hasattr(con,'rollback'):
+        if hasattr(con, "rollback"):
             try:
                 con.rollback()
             except self.driver.NotSupportedError:
@@ -242,7 +226,7 @@ class DatabaseAPI20Test(unittest.TestCase):
     def test_cursor(self):
         con = self._connect()
         try:
-            cur = con.cursor()
+            _ = con.cursor()
         finally:
             con.close()
 
@@ -254,14 +238,14 @@ class DatabaseAPI20Test(unittest.TestCase):
             cur1 = con.cursor()
             cur2 = con.cursor()
             self.executeDDL1(cur1)
-            cur1.execute("insert into %sbooze values ('Victoria Bitter')" % (
-                self.table_prefix
-                ))
+            cur1.execute(
+                "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix)
+            )
             cur2.execute("select name from %sbooze" % self.table_prefix)
             booze = cur2.fetchall()
-            self.assertEqual(len(booze),1)
-            self.assertEqual(len(booze[0]),1)
-            self.assertEqual(booze[0][0],'Victoria Bitter')
+            self.assertEqual(len(booze), 1)
+            self.assertEqual(len(booze[0]), 1)
+            self.assertEqual(booze[0][0], "Victoria Bitter")
         finally:
             con.close()
 
@@ -270,31 +254,41 @@ class DatabaseAPI20Test(unittest.TestCase):
         try:
             cur = con.cursor()
             self.executeDDL1(cur)
-            self.assertEqual(cur.description,None,
-                'cursor.description should be none after executing a '
-                'statement that can return no rows (such as DDL)'
-                )
-            cur.execute('select name from %sbooze' % self.table_prefix)
-            self.assertEqual(len(cur.description),1,
-                'cursor.description describes too many columns'
-                )
-            self.assertEqual(len(cur.description[0]),7,
-                'cursor.description[x] tuples must have 7 elements'
-                )
-            self.assertEqual(cur.description[0][0].lower(),'name',
-                'cursor.description[x][0] must return column name'
-                )
-            self.assertEqual(cur.description[0][1],self.driver.STRING,
-                'cursor.description[x][1] must return column type. Got %r'
-                    % cur.description[0][1]
-                )
+            self.assertEqual(
+                cur.description,
+                None,
+                "cursor.description should be none after executing a "
+                "statement that can return no rows (such as DDL)",
+            )
+            cur.execute("select name from %sbooze" % self.table_prefix)
+            self.assertEqual(
+                len(cur.description), 1, "cursor.description describes too many columns"
+            )
+            self.assertEqual(
+                len(cur.description[0]),
+                7,
+                "cursor.description[x] tuples must have 7 elements",
+            )
+            self.assertEqual(
+                cur.description[0][0].lower(),
+                "name",
+                "cursor.description[x][0] must return column name",
+            )
+            self.assertEqual(
+                cur.description[0][1],
+                self.driver.STRING,
+                "cursor.description[x][1] must return column type. Got %r"
+                % cur.description[0][1],
+            )
 
             # Make sure self.description gets reset
             self.executeDDL2(cur)
-            self.assertEqual(cur.description,None,
-                'cursor.description not being set to None when executing '
-                'no-result statements (eg. DDL)'
-                )
+            self.assertEqual(
+                cur.description,
+                None,
+                "cursor.description not being set to None when executing "
+                "no-result statements (eg. DDL)",
+            )
         finally:
             con.close()
 
@@ -303,47 +297,49 @@ class DatabaseAPI20Test(unittest.TestCase):
         try:
             cur = con.cursor()
             self.executeDDL1(cur)
-            self.assertEqual(cur.rowcount,-1,
-                'cursor.rowcount should be -1 after executing no-result '
-                'statements'
-                )
-            cur.execute("insert into %sbooze values ('Victoria Bitter')" % (
-                self.table_prefix
-                ))
-            self.assertTrue(cur.rowcount in (-1,1),
-                'cursor.rowcount should == number or rows inserted, or '
-                'set to -1 after executing an insert statement'
-                )
+            self.assertEqual(
+                cur.rowcount,
+                -1,
+                "cursor.rowcount should be -1 after executing no-result " "statements",
+            )
+            cur.execute(
+                "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix)
+            )
+            self.assertTrue(
+                cur.rowcount in (-1, 1),
+                "cursor.rowcount should == number or rows inserted, or "
+                "set to -1 after executing an insert statement",
+            )
             cur.execute("select name from %sbooze" % self.table_prefix)
-            self.assertTrue(cur.rowcount in (-1,1),
-                'cursor.rowcount should == number of rows returned, or '
-                'set to -1 after executing a select statement'
-                )
+            self.assertTrue(
+                cur.rowcount in (-1, 1),
+                "cursor.rowcount should == number of rows returned, or "
+                "set to -1 after executing a select statement",
+            )
             self.executeDDL2(cur)
-            self.assertEqual(cur.rowcount,-1,
-                'cursor.rowcount not being reset to -1 after executing '
-                'no-result statements'
-                )
+            self.assertEqual(
+                cur.rowcount,
+                -1,
+                "cursor.rowcount not being reset to -1 after executing "
+                "no-result statements",
+            )
         finally:
             con.close()
 
-    lower_func = 'lower'
+    lower_func = "lower"
+
     def test_callproc(self):
         con = self._connect()
         try:
             cur = con.cursor()
-            if self.lower_func and hasattr(cur,'callproc'):
-                r = cur.callproc(self.lower_func,('FOO',))
-                self.assertEqual(len(r),1)
-                self.assertEqual(r[0],'FOO')
+            if self.lower_func and hasattr(cur, "callproc"):
+                r = cur.callproc(self.lower_func, ("FOO",))
+                self.assertEqual(len(r), 1)
+                self.assertEqual(r[0], "FOO")
                 r = cur.fetchall()
-                self.assertEqual(len(r),1,'callproc produced no result set')
-                self.assertEqual(len(r[0]),1,
-                    'callproc produced invalid result set'
-                    )
-                self.assertEqual(r[0][0],'foo',
-                    'callproc produced invalid results'
-                    )
+                self.assertEqual(len(r), 1, "callproc produced no result set")
+                self.assertEqual(len(r[0]), 1, "callproc produced invalid result set")
+                self.assertEqual(r[0][0], "foo", "callproc produced invalid results")
         finally:
             con.close()
 
@@ -356,14 +352,14 @@ class DatabaseAPI20Test(unittest.TestCase):
 
         # cursor.execute should raise an Error if called after connection
         # closed
-        self.assertRaises(self.driver.Error,self.executeDDL1,cur)
+        self.assertRaises(self.driver.Error, self.executeDDL1, cur)
 
         # connection.commit should raise an Error if called after connection'
         # closed.'
-        self.assertRaises(self.driver.Error,con.commit)
+        self.assertRaises(self.driver.Error, con.commit)
 
         # connection.close should raise an Error if called more than once
-        self.assertRaises(self.driver.Error,con.close)
+        self.assertRaises(self.driver.Error, con.close)
 
     def test_execute(self):
         con = self._connect()
@@ -373,105 +369,99 @@ class DatabaseAPI20Test(unittest.TestCase):
         finally:
             con.close()
 
-    def _paraminsert(self,cur):
+    def _paraminsert(self, cur):
         self.executeDDL1(cur)
-        cur.execute("insert into %sbooze values ('Victoria Bitter')" % (
-            self.table_prefix
-            ))
-        self.assertTrue(cur.rowcount in (-1,1))
+        cur.execute(
+            "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix)
+        )
+        self.assertTrue(cur.rowcount in (-1, 1))
 
-        if self.driver.paramstyle == 'qmark':
+        if self.driver.paramstyle == "qmark":
             cur.execute(
-                'insert into %sbooze values (?)' % self.table_prefix,
-                ("Cooper's",)
-                )
-        elif self.driver.paramstyle == 'numeric':
+                "insert into %sbooze values (?)" % self.table_prefix, ("Cooper's",)
+            )
+        elif self.driver.paramstyle == "numeric":
             cur.execute(
-                'insert into %sbooze values (:1)' % self.table_prefix,
-                ("Cooper's",)
-                )
-        elif self.driver.paramstyle == 'named':
+                "insert into %sbooze values (:1)" % self.table_prefix, ("Cooper's",)
+            )
+        elif self.driver.paramstyle == "named":
             cur.execute(
-                'insert into %sbooze values (:beer)' % self.table_prefix,
-                {'beer':"Cooper's"}
-                )
-        elif self.driver.paramstyle == 'format':
+                "insert into %sbooze values (:beer)" % self.table_prefix,
+                {"beer": "Cooper's"},
+            )
+        elif self.driver.paramstyle == "format":
             cur.execute(
-                'insert into %sbooze values (%%s)' % self.table_prefix,
-                ("Cooper's",)
-                )
-        elif self.driver.paramstyle == 'pyformat':
+                "insert into %sbooze values (%%s)" % self.table_prefix, ("Cooper's",)
+            )
+        elif self.driver.paramstyle == "pyformat":
             cur.execute(
-                'insert into %sbooze values (%%(beer)s)' % self.table_prefix,
-                {'beer':"Cooper's"}
-                )
+                "insert into %sbooze values (%%(beer)s)" % self.table_prefix,
+                {"beer": "Cooper's"},
+            )
         else:
-            self.fail('Invalid paramstyle')
-        self.assertTrue(cur.rowcount in (-1,1))
+            self.fail("Invalid paramstyle")
+        self.assertTrue(cur.rowcount in (-1, 1))
 
-        cur.execute('select name from %sbooze' % self.table_prefix)
+        cur.execute("select name from %sbooze" % self.table_prefix)
         res = cur.fetchall()
-        self.assertEqual(len(res),2,'cursor.fetchall returned too few rows')
-        beers = [res[0][0],res[1][0]]
+        self.assertEqual(len(res), 2, "cursor.fetchall returned too few rows")
+        beers = [res[0][0], res[1][0]]
         beers.sort()
-        self.assertEqual(beers[0],"Cooper's",
-            'cursor.fetchall retrieved incorrect data, or data inserted '
-            'incorrectly'
-            )
-        self.assertEqual(beers[1],"Victoria Bitter",
-            'cursor.fetchall retrieved incorrect data, or data inserted '
-            'incorrectly'
-            )
+        self.assertEqual(
+            beers[0],
+            "Cooper's",
+            "cursor.fetchall retrieved incorrect data, or data inserted " "incorrectly",
+        )
+        self.assertEqual(
+            beers[1],
+            "Victoria Bitter",
+            "cursor.fetchall retrieved incorrect data, or data inserted " "incorrectly",
+        )
 
     def test_executemany(self):
         con = self._connect()
         try:
             cur = con.cursor()
             self.executeDDL1(cur)
-            largs = [ ("Cooper's",) , ("Boag's",) ]
-            margs = [ {'beer': "Cooper's"}, {'beer': "Boag's"} ]
-            if self.driver.paramstyle == 'qmark':
+            largs = [("Cooper's",), ("Boag's",)]
+            margs = [{"beer": "Cooper's"}, {"beer": "Boag's"}]
+            if self.driver.paramstyle == "qmark":
                 cur.executemany(
-                    'insert into %sbooze values (?)' % self.table_prefix,
-                    largs
-                    )
-            elif self.driver.paramstyle == 'numeric':
+                    "insert into %sbooze values (?)" % self.table_prefix, largs
+                )
+            elif self.driver.paramstyle == "numeric":
                 cur.executemany(
-                    'insert into %sbooze values (:1)' % self.table_prefix,
-                    largs
-                    )
-            elif self.driver.paramstyle == 'named':
+                    "insert into %sbooze values (:1)" % self.table_prefix, largs
+                )
+            elif self.driver.paramstyle == "named":
                 cur.executemany(
-                    'insert into %sbooze values (:beer)' % self.table_prefix,
-                    margs
-                    )
-            elif self.driver.paramstyle == 'format':
+                    "insert into %sbooze values (:beer)" % self.table_prefix, margs
+                )
+            elif self.driver.paramstyle == "format":
                 cur.executemany(
-                    'insert into %sbooze values (%%s)' % self.table_prefix,
-                    largs
-                    )
-            elif self.driver.paramstyle == 'pyformat':
+                    "insert into %sbooze values (%%s)" % self.table_prefix, largs
+                )
+            elif self.driver.paramstyle == "pyformat":
                 cur.executemany(
-                    'insert into %sbooze values (%%(beer)s)' % (
-                        self.table_prefix
-                        ),
-                    margs
-                    )
-            else:
-                self.fail('Unknown paramstyle')
-            self.assertTrue(cur.rowcount in (-1,2),
-                'insert using cursor.executemany set cursor.rowcount to '
-                'incorrect value %r' % cur.rowcount
+                    "insert into %sbooze values (%%(beer)s)" % (self.table_prefix),
+                    margs,
                 )
-            cur.execute('select name from %sbooze' % self.table_prefix)
+            else:
+                self.fail("Unknown paramstyle")
+            self.assertTrue(
+                cur.rowcount in (-1, 2),
+                "insert using cursor.executemany set cursor.rowcount to "
+                "incorrect value %r" % cur.rowcount,
+            )
+            cur.execute("select name from %sbooze" % self.table_prefix)
             res = cur.fetchall()
-            self.assertEqual(len(res),2,
-                'cursor.fetchall retrieved incorrect number of rows'
-                )
-            beers = [res[0][0],res[1][0]]
+            self.assertEqual(
+                len(res), 2, "cursor.fetchall retrieved incorrect number of rows"
+            )
+            beers = [res[0][0], res[1][0]]
             beers.sort()
-            self.assertEqual(beers[0],"Boag's",'incorrect data retrieved')
-            self.assertEqual(beers[1],"Cooper's",'incorrect data retrieved')
+            self.assertEqual(beers[0], "Boag's", "incorrect data retrieved")
+            self.assertEqual(beers[1], "Cooper's", "incorrect data retrieved")
         finally:
             con.close()
 
@@ -482,59 +472,62 @@ class DatabaseAPI20Test(unittest.TestCase):
 
             # cursor.fetchone should raise an Error if called before
             # executing a select-type query
-            self.assertRaises(self.driver.Error,cur.fetchone)
+            self.assertRaises(self.driver.Error, cur.fetchone)
 
             # cursor.fetchone should raise an Error if called after
             # executing a query that cannot return rows
             self.executeDDL1(cur)
-            self.assertRaises(self.driver.Error,cur.fetchone)
+            self.assertRaises(self.driver.Error, cur.fetchone)
 
-            cur.execute('select name from %sbooze' % self.table_prefix)
-            self.assertEqual(cur.fetchone(),None,
-                'cursor.fetchone should return None if a query retrieves '
-                'no rows'
-                )
-            self.assertTrue(cur.rowcount in (-1,0))
+            cur.execute("select name from %sbooze" % self.table_prefix)
+            self.assertEqual(
+                cur.fetchone(),
+                None,
+                "cursor.fetchone should return None if a query retrieves " "no rows",
+            )
+            self.assertTrue(cur.rowcount in (-1, 0))
 
             # cursor.fetchone should raise an Error if called after
             # executing a query that cannot return rows
-            cur.execute("insert into %sbooze values ('Victoria Bitter')" % (
-                self.table_prefix
-                ))
-            self.assertRaises(self.driver.Error,cur.fetchone)
+            cur.execute(
+                "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix)
+            )
+            self.assertRaises(self.driver.Error, cur.fetchone)
 
-            cur.execute('select name from %sbooze' % self.table_prefix)
+            cur.execute("select name from %sbooze" % self.table_prefix)
             r = cur.fetchone()
-            self.assertEqual(len(r),1,
-                'cursor.fetchone should have retrieved a single row'
-                )
-            self.assertEqual(r[0],'Victoria Bitter',
-                'cursor.fetchone retrieved incorrect data'
-                )
-            self.assertEqual(cur.fetchone(),None,
-                'cursor.fetchone should return None if no more rows available'
-                )
-            self.assertTrue(cur.rowcount in (-1,1))
+            self.assertEqual(
+                len(r), 1, "cursor.fetchone should have retrieved a single row"
+            )
+            self.assertEqual(
+                r[0], "Victoria Bitter", "cursor.fetchone retrieved incorrect data"
+            )
+            self.assertEqual(
+                cur.fetchone(),
+                None,
+                "cursor.fetchone should return None if no more rows available",
+            )
+            self.assertTrue(cur.rowcount in (-1, 1))
         finally:
             con.close()
 
     samples = [
-        'Carlton Cold',
-        'Carlton Draft',
-        'Mountain Goat',
-        'Redback',
-        'Victoria Bitter',
-        'XXXX'
-        ]
+        "Carlton Cold",
+        "Carlton Draft",
+        "Mountain Goat",
+        "Redback",
+        "Victoria Bitter",
+        "XXXX",
+    ]
 
     def _populate(self):
-        ''' Return a list of sql commands to setup the DB for the fetch
-            tests.
-        '''
+        """Return a list of sql commands to setup the DB for the fetch
+        tests.
+        """
         populate = [
-            "insert into %sbooze values ('%s')" % (self.table_prefix,s)
-                for s in self.samples
-            ]
+            "insert into {}booze values ('{}')".format(self.table_prefix, s)
+            for s in self.samples
+        ]
         return populate
 
     def test_fetchmany(self):
@@ -543,78 +536,88 @@ class DatabaseAPI20Test(unittest.TestCase):
             cur = con.cursor()
 
             # cursor.fetchmany should raise an Error if called without
-            #issuing a query
-            self.assertRaises(self.driver.Error,cur.fetchmany,4)
+            # issuing a query
+            self.assertRaises(self.driver.Error, cur.fetchmany, 4)
 
             self.executeDDL1(cur)
             for sql in self._populate():
                 cur.execute(sql)
 
-            cur.execute('select name from %sbooze' % self.table_prefix)
+            cur.execute("select name from %sbooze" % self.table_prefix)
             r = cur.fetchmany()
-            self.assertEqual(len(r),1,
-                'cursor.fetchmany retrieved incorrect number of rows, '
-                'default of arraysize is one.'
-                )
-            cur.arraysize=10
-            r = cur.fetchmany(3) # Should get 3 rows
-            self.assertEqual(len(r),3,
-                'cursor.fetchmany retrieved incorrect number of rows'
-                )
-            r = cur.fetchmany(4) # Should get 2 more
-            self.assertEqual(len(r),2,
-                'cursor.fetchmany retrieved incorrect number of rows'
-                )
-            r = cur.fetchmany(4) # Should be an empty sequence
-            self.assertEqual(len(r),0,
-                'cursor.fetchmany should return an empty sequence after '
-                'results are exhausted'
+            self.assertEqual(
+                len(r),
+                1,
+                "cursor.fetchmany retrieved incorrect number of rows, "
+                "default of arraysize is one.",
+            )
+            cur.arraysize = 10
+            r = cur.fetchmany(3)  # Should get 3 rows
+            self.assertEqual(
+                len(r), 3, "cursor.fetchmany retrieved incorrect number of rows"
+            )
+            r = cur.fetchmany(4)  # Should get 2 more
+            self.assertEqual(
+                len(r), 2, "cursor.fetchmany retrieved incorrect number of rows"
             )
-            self.assertTrue(cur.rowcount in (-1,6))
+            r = cur.fetchmany(4)  # Should be an empty sequence
+            self.assertEqual(
+                len(r),
+                0,
+                "cursor.fetchmany should return an empty sequence after "
+                "results are exhausted",
+            )
+            self.assertTrue(cur.rowcount in (-1, 6))
 
             # Same as above, using cursor.arraysize
-            cur.arraysize=4
-            cur.execute('select name from %sbooze' % self.table_prefix)
-            r = cur.fetchmany() # Should get 4 rows
-            self.assertEqual(len(r),4,
-                'cursor.arraysize not being honoured by fetchmany'
-                )
-            r = cur.fetchmany() # Should get 2 more
-            self.assertEqual(len(r),2)
-            r = cur.fetchmany() # Should be an empty sequence
-            self.assertEqual(len(r),0)
-            self.assertTrue(cur.rowcount in (-1,6))
-
-            cur.arraysize=6
-            cur.execute('select name from %sbooze' % self.table_prefix)
-            rows = cur.fetchmany() # Should get all rows
-            self.assertTrue(cur.rowcount in (-1,6))
-            self.assertEqual(len(rows),6)
-            self.assertEqual(len(rows),6)
+            cur.arraysize = 4
+            cur.execute("select name from %sbooze" % self.table_prefix)
+            r = cur.fetchmany()  # Should get 4 rows
+            self.assertEqual(
+                len(r), 4, "cursor.arraysize not being honoured by fetchmany"
+            )
+            r = cur.fetchmany()  # Should get 2 more
+            self.assertEqual(len(r), 2)
+            r = cur.fetchmany()  # Should be an empty sequence
+            self.assertEqual(len(r), 0)
+            self.assertTrue(cur.rowcount in (-1, 6))
+
+            cur.arraysize = 6
+            cur.execute("select name from %sbooze" % self.table_prefix)
+            rows = cur.fetchmany()  # Should get all rows
+            self.assertTrue(cur.rowcount in (-1, 6))
+            self.assertEqual(len(rows), 6)
+            self.assertEqual(len(rows), 6)
             rows = [r[0] for r in rows]
             rows.sort()
 
             # Make sure we get the right data back out
-            for i in range(0,6):
-                self.assertEqual(rows[i],self.samples[i],
-                    'incorrect data retrieved by cursor.fetchmany'
-                    )
-
-            rows = cur.fetchmany() # Should return an empty list
-            self.assertEqual(len(rows),0,
-                'cursor.fetchmany should return an empty sequence if '
-                'called after the whole result set has been fetched'
+            for i in range(0, 6):
+                self.assertEqual(
+                    rows[i],
+                    self.samples[i],
+                    "incorrect data retrieved by cursor.fetchmany",
                 )
-            self.assertTrue(cur.rowcount in (-1,6))
+
+            rows = cur.fetchmany()  # Should return an empty list
+            self.assertEqual(
+                len(rows),
+                0,
+                "cursor.fetchmany should return an empty sequence if "
+                "called after the whole result set has been fetched",
+            )
+            self.assertTrue(cur.rowcount in (-1, 6))
 
             self.executeDDL2(cur)
-            cur.execute('select name from %sbarflys' % self.table_prefix)
-            r = cur.fetchmany() # Should get empty sequence
-            self.assertEqual(len(r),0,
-                'cursor.fetchmany should return an empty sequence if '
-                'query retrieved no rows'
-                )
-            self.assertTrue(cur.rowcount in (-1,0))
+            cur.execute("select name from %sbarflys" % self.table_prefix)
+            r = cur.fetchmany()  # Should get empty sequence
+            self.assertEqual(
+                len(r),
+                0,
+                "cursor.fetchmany should return an empty sequence if "
+                "query retrieved no rows",
+            )
+            self.assertTrue(cur.rowcount in (-1, 0))
 
         finally:
             con.close()
@@ -634,36 +637,41 @@ class DatabaseAPI20Test(unittest.TestCase):
 
             # cursor.fetchall should raise an Error if called
             # after executing a statement that cannot return rows
-            self.assertRaises(self.driver.Error,cur.fetchall)
+            self.assertRaises(self.driver.Error, cur.fetchall)
 
-            cur.execute('select name from %sbooze' % self.table_prefix)
+            cur.execute("select name from %sbooze" % self.table_prefix)
             rows = cur.fetchall()
-            self.assertTrue(cur.rowcount in (-1,len(self.samples)))
-            self.assertEqual(len(rows),len(self.samples),
-                'cursor.fetchall did not retrieve all rows'
-                )
+            self.assertTrue(cur.rowcount in (-1, len(self.samples)))
+            self.assertEqual(
+                len(rows),
+                len(self.samples),
+                "cursor.fetchall did not retrieve all rows",
+            )
             rows = [r[0] for r in rows]
             rows.sort()
-            for i in range(0,len(self.samples)):
-                self.assertEqual(rows[i],self.samples[i],
-                'cursor.fetchall retrieved incorrect rows'
+            for i in range(0, len(self.samples)):
+                self.assertEqual(
+                    rows[i], self.samples[i], "cursor.fetchall retrieved incorrect rows"
                 )
             rows = cur.fetchall()
             self.assertEqual(
-                len(rows),0,
-                'cursor.fetchall should return an empty list if called '
-                'after the whole result set has been fetched'
-                )
-            self.assertTrue(cur.rowcount in (-1,len(self.samples)))
+                len(rows),
+                0,
+                "cursor.fetchall should return an empty list if called "
+                "after the whole result set has been fetched",
+            )
+            self.assertTrue(cur.rowcount in (-1, len(self.samples)))
 
             self.executeDDL2(cur)
-            cur.execute('select name from %sbarflys' % self.table_prefix)
+            cur.execute("select name from %sbarflys" % self.table_prefix)
             rows = cur.fetchall()
-            self.assertTrue(cur.rowcount in (-1,0))
-            self.assertEqual(len(rows),0,
-                'cursor.fetchall should return an empty list if '
-                'a select query returns no rows'
-                )
+            self.assertTrue(cur.rowcount in (-1, 0))
+            self.assertEqual(
+                len(rows),
+                0,
+                "cursor.fetchall should return an empty list if "
+                "a select query returns no rows",
+            )
 
         finally:
             con.close()
@@ -676,91 +684,91 @@ class DatabaseAPI20Test(unittest.TestCase):
             for sql in self._populate():
                 cur.execute(sql)
 
-            cur.execute('select name from %sbooze' % self.table_prefix)
-            rows1  = cur.fetchone()
+            cur.execute("select name from %sbooze" % self.table_prefix)
+            rows1 = cur.fetchone()
             rows23 = cur.fetchmany(2)
-            rows4  = cur.fetchone()
+            rows4 = cur.fetchone()
             rows56 = cur.fetchall()
-            self.assertTrue(cur.rowcount in (-1,6))
-            self.assertEqual(len(rows23),2,
-                'fetchmany returned incorrect number of rows'
-                )
-            self.assertEqual(len(rows56),2,
-                'fetchall returned incorrect number of rows'
-                )
+            self.assertTrue(cur.rowcount in (-1, 6))
+            self.assertEqual(
+                len(rows23), 2, "fetchmany returned incorrect number of rows"
+            )
+            self.assertEqual(
+                len(rows56), 2, "fetchall returned incorrect number of rows"
+            )
 
             rows = [rows1[0]]
-            rows.extend([rows23[0][0],rows23[1][0]])
+            rows.extend([rows23[0][0], rows23[1][0]])
             rows.append(rows4[0])
-            rows.extend([rows56[0][0],rows56[1][0]])
+            rows.extend([rows56[0][0], rows56[1][0]])
             rows.sort()
-            for i in range(0,len(self.samples)):
-                self.assertEqual(rows[i],self.samples[i],
-                    'incorrect data retrieved or inserted'
-                    )
+            for i in range(0, len(self.samples)):
+                self.assertEqual(
+                    rows[i], self.samples[i], "incorrect data retrieved or inserted"
+                )
         finally:
             con.close()
 
-    def help_nextset_setUp(self,cur):
-        ''' Should create a procedure called deleteme
-            that returns two result sets, first the
-            number of rows in booze then "name from booze"
-        '''
-        raise NotImplementedError('Helper not implemented')
-        #sql="""
+    def help_nextset_setUp(self, cur):
+        """Should create a procedure called deleteme
+        that returns two result sets, first the
+        number of rows in booze then "name from booze"
+        """
+        raise NotImplementedError("Helper not implemented")
+        # sql="""
         #    create procedure deleteme as
         #    begin
         #        select count(*) from booze
         #        select name from booze
         #    end
-        #"""
-        #cur.execute(sql)
+        # """
+        # cur.execute(sql)
 
-    def help_nextset_tearDown(self,cur):
-        'If cleaning up is needed after nextSetTest'
-        raise NotImplementedError('Helper not implemented')
-        #cur.execute("drop procedure deleteme")
+    def help_nextset_tearDown(self, cur):
+        "If cleaning up is needed after nextSetTest"
+        raise NotImplementedError("Helper not implemented")
+        # cur.execute("drop procedure deleteme")
 
     def test_nextset(self):
         con = self._connect()
         try:
             cur = con.cursor()
-            if not hasattr(cur,'nextset'):
+            if not hasattr(cur, "nextset"):
                 return
 
             try:
                 self.executeDDL1(cur)
-                sql=self._populate()
+                sql = self._populate()
                 for sql in self._populate():
                     cur.execute(sql)
 
                 self.help_nextset_setUp(cur)
 
-                cur.callproc('deleteme')
-                numberofrows=cur.fetchone()
-                assert numberofrows[0]== len(self.samples)
+                cur.callproc("deleteme")
+                numberofrows = cur.fetchone()
+                assert numberofrows[0] == len(self.samples)
                 assert cur.nextset()
-                names=cur.fetchall()
+                names = cur.fetchall()
                 assert len(names) == len(self.samples)
-                s=cur.nextset()
-                assert s == None,'No more return sets, should return None'
+                s = cur.nextset()
+                assert s is None, "No more return sets, should return None"
             finally:
                 self.help_nextset_tearDown(cur)
 
         finally:
             con.close()
 
-    def test_nextset(self):
-        raise NotImplementedError('Drivers need to override this test')
+    def test_nextset(self):  # noqa: F811
+        raise NotImplementedError("Drivers need to override this test")
 
     def test_arraysize(self):
         # Not much here - rest of the tests for this are in test_fetchmany
         con = self._connect()
         try:
             cur = con.cursor()
-            self.assertTrue(hasattr(cur,'arraysize'),
-                'cursor.arraysize must be defined'
-                )
+            self.assertTrue(
+                hasattr(cur, "arraysize"), "cursor.arraysize must be defined"
+            )
         finally:
             con.close()
 
@@ -768,8 +776,8 @@ class DatabaseAPI20Test(unittest.TestCase):
         con = self._connect()
         try:
             cur = con.cursor()
-            cur.setinputsizes( (25,) )
-            self._paraminsert(cur) # Make sure cursor still works
+            cur.setinputsizes((25,))
+            self._paraminsert(cur)  # Make sure cursor still works
         finally:
             con.close()
 
@@ -779,75 +787,74 @@ class DatabaseAPI20Test(unittest.TestCase):
         try:
             cur = con.cursor()
             cur.setoutputsize(1000)
-            cur.setoutputsize(2000,0)
-            self._paraminsert(cur) # Make sure the cursor still works
+            cur.setoutputsize(2000, 0)
+            self._paraminsert(cur)  # Make sure the cursor still works
         finally:
             con.close()
 
     def test_setoutputsize(self):
-        # Real test for setoutputsize is driver dependant
-        raise NotImplementedError('Driver need to override this test')
+        # Real test for setoutputsize is driver dependent
+        raise NotImplementedError("Driver need to override this test")
 
     def test_None(self):
         con = self._connect()
         try:
             cur = con.cursor()
             self.executeDDL1(cur)
-            cur.execute('insert into %sbooze values (NULL)' % self.table_prefix)
-            cur.execute('select name from %sbooze' % self.table_prefix)
+            cur.execute("insert into %sbooze values (NULL)" % self.table_prefix)
+            cur.execute("select name from %sbooze" % self.table_prefix)
             r = cur.fetchall()
-            self.assertEqual(len(r),1)
-            self.assertEqual(len(r[0]),1)
-            self.assertEqual(r[0][0],None,'NULL value not returned as None')
+            self.assertEqual(len(r), 1)
+            self.assertEqual(len(r[0]), 1)
+            self.assertEqual(r[0][0], None, "NULL value not returned as None")
         finally:
             con.close()
 
     def test_Date(self):
-        d1 = self.driver.Date(2002,12,25)
-        d2 = self.driver.DateFromTicks(time.mktime((2002,12,25,0,0,0,0,0,0)))
+        d1 = self.driver.Date(2002, 12, 25)  # noqa F841
+        d2 = self.driver.DateFromTicks(  # noqa F841
+            time.mktime((2002, 12, 25, 0, 0, 0, 0, 0, 0))
+        )
         # Can we assume this? API doesn't specify, but it seems implied
         # self.assertEqual(str(d1),str(d2))
 
     def test_Time(self):
-        t1 = self.driver.Time(13,45,30)
-        t2 = self.driver.TimeFromTicks(time.mktime((2001,1,1,13,45,30,0,0,0)))
+        t1 = self.driver.Time(13, 45, 30)  # noqa F841
+        t2 = self.driver.TimeFromTicks(  # noqa F841
+            time.mktime((2001, 1, 1, 13, 45, 30, 0, 0, 0))
+        )
         # Can we assume this? API doesn't specify, but it seems implied
         # self.assertEqual(str(t1),str(t2))
 
     def test_Timestamp(self):
-        t1 = self.driver.Timestamp(2002,12,25,13,45,30)
-        t2 = self.driver.TimestampFromTicks(
-            time.mktime((2002,12,25,13,45,30,0,0,0))
-            )
+        t1 = self.driver.Timestamp(2002, 12, 25, 13, 45, 30)  # noqa F841
+        t2 = self.driver.TimestampFromTicks(  # noqa F841
+            time.mktime((2002, 12, 25, 13, 45, 30, 0, 0, 0))
+        )
         # Can we assume this? API doesn't specify, but it seems implied
         # self.assertEqual(str(t1),str(t2))
 
     def test_Binary(self):
-        b = self.driver.Binary(b'Something')
-        b = self.driver.Binary(b'')
+        b = self.driver.Binary(b"Something")
+        b = self.driver.Binary(b"")  # noqa F841
 
     def test_STRING(self):
-        self.assertTrue(hasattr(self.driver,'STRING'),
-            'module.STRING must be defined'
-            )
+        self.assertTrue(hasattr(self.driver, "STRING"), "module.STRING must be defined")
 
     def test_BINARY(self):
-        self.assertTrue(hasattr(self.driver,'BINARY'),
-            'module.BINARY must be defined.'
-            )
+        self.assertTrue(
+            hasattr(self.driver, "BINARY"), "module.BINARY must be defined."
+        )
 
     def test_NUMBER(self):
-        self.assertTrue(hasattr(self.driver,'NUMBER'),
-            'module.NUMBER must be defined.'
-            )
+        self.assertTrue(
+            hasattr(self.driver, "NUMBER"), "module.NUMBER must be defined."
+        )
 
     def test_DATETIME(self):
-        self.assertTrue(hasattr(self.driver,'DATETIME'),
-            'module.DATETIME must be defined.'
-            )
+        self.assertTrue(
+            hasattr(self.driver, "DATETIME"), "module.DATETIME must be defined."
+        )
 
     def test_ROWID(self):
-        self.assertTrue(hasattr(self.driver,'ROWID'),
-            'module.ROWID must be defined.'
-            )
-
+        self.assertTrue(hasattr(self.driver, "ROWID"), "module.ROWID must be defined.")
diff --git a/tests/test_MySQLdb_capabilities.py b/tests/test_MySQLdb_capabilities.py
index 6e39d14..fc213b8 100644
--- a/tests/test_MySQLdb_capabilities.py
+++ b/tests/test_MySQLdb_capabilities.py
@@ -1,24 +1,23 @@
 #!/usr/bin/env python
-# -*- coding: utf-8 -*-
 import capabilities
 from datetime import timedelta
 from contextlib import closing
 import unittest
 import MySQLdb
-from MySQLdb.compat import unicode
-from MySQLdb import cursors
 from configdb import connection_factory
 import warnings
 
 
-warnings.filterwarnings('ignore')
+warnings.filterwarnings("ignore")
 
 
 class test_MySQLdb(capabilities.DatabaseTest):
 
     db_module = MySQLdb
     connect_args = ()
-    connect_kwargs = dict(use_unicode=True, sql_mode="ANSI,STRICT_TRANS_TABLES,TRADITIONAL")
+    connect_kwargs = dict(
+        use_unicode=True, sql_mode="ANSI,STRICT_TRANS_TABLES,TRADITIONAL"
+    )
     create_table_extra = "ENGINE=INNODB CHARACTER SET UTF8"
     leak_test = False
 
@@ -26,99 +25,115 @@ class test_MySQLdb(capabilities.DatabaseTest):
         return "`%s`" % ident
 
     def test_TIME(self):
-        def generator(row,col):
-            return timedelta(0, row*8000)
-        self.check_data_integrity(
-                 ('col1 TIME',),
-                 generator)
+        def generator(row, col):
+            return timedelta(0, row * 8000)
+
+        self.check_data_integrity(("col1 TIME",), generator)
 
     def test_TINYINT(self):
         # Number data
         def generator(row, col):
-            v = (row*row) % 256
+            v = (row * row) % 256
             if v > 127:
-                v = v-256
+                v = v - 256
             return v
-        self.check_data_integrity(
-            ('col1 TINYINT',),
-            generator)
+
+        self.check_data_integrity(("col1 TINYINT",), generator)
 
     def test_stored_procedures(self):
         db = self.connection
         c = self.cursor
-        self.create_table(('pos INT', 'tree CHAR(20)'))
-        c.executemany("INSERT INTO %s (pos,tree) VALUES (%%s,%%s)" % self.table,
-                      list(enumerate('ash birch cedar Lärche pine'.split())))
+        self.create_table(("pos INT", "tree CHAR(20)"))
+        c.executemany(
+            "INSERT INTO %s (pos,tree) VALUES (%%s,%%s)" % self.table,
+            list(enumerate("ash birch cedar Lärche pine".split())),
+        )
         db.commit()
 
-        c.execute("""
+        c.execute(
+            """
         CREATE PROCEDURE test_sp(IN t VARCHAR(255))
         BEGIN
             SELECT pos FROM %s WHERE tree = t;
         END
-        """ % self.table)
+        """
+            % self.table
+        )
         db.commit()
 
-        c.callproc('test_sp', ('Lärche',))
+        c.callproc("test_sp", ("Lärche",))
         rows = c.fetchall()
         self.assertEqual(len(rows), 1)
         self.assertEqual(rows[0][0], 3)
         c.nextset()
 
         c.execute("DROP PROCEDURE test_sp")
-        c.execute('drop table %s' % (self.table))
+        c.execute("drop table %s" % (self.table))
 
     def test_small_CHAR(self):
         # Character data
-        def generator(row,col):
-            i = (row*col+62)%256
-            if i == 62: return ''
-            if i == 63: return None
+        def generator(row, col):
+            i = (row * col + 62) % 256
+            if i == 62:
+                return ""
+            if i == 63:
+                return None
             return chr(i)
-        self.check_data_integrity(
-            ('col1 char(1)','col2 char(1)'),
-            generator)
+
+        self.check_data_integrity(("col1 char(1)", "col2 char(1)"), generator)
 
     def test_BIT(self):
         c = self.cursor
         try:
-            c.execute("""create table test_BIT (
+            c.execute(
+                """create table test_BIT (
                 b3 BIT(3),
                 b7 BIT(10),
-                b64 BIT(64))""")
+                b64 BIT(64))"""
+            )
 
-            one64 = '1'*64
+            one64 = "1" * 64
             c.execute(
                 "insert into test_BIT (b3, b7, b64)"
-                " VALUES (b'011', b'1111111111', b'%s')"
-                % one64)
+                " VALUES (b'011', b'1111111111', b'%s')" % one64
+            )
 
             c.execute("SELECT b3, b7, b64 FROM test_BIT")
             row = c.fetchone()
-            self.assertEqual(row[0], b'\x03')
-            self.assertEqual(row[1], b'\x03\xff')
-            self.assertEqual(row[2], b'\xff'*8)
+            self.assertEqual(row[0], b"\x03")
+            self.assertEqual(row[1], b"\x03\xff")
+            self.assertEqual(row[2], b"\xff" * 8)
         finally:
             c.execute("drop table if exists test_BIT")
 
     def test_MULTIPOLYGON(self):
         c = self.cursor
         try:
-            c.execute("""create table test_MULTIPOLYGON (
+            c.execute(
+                """create table test_MULTIPOLYGON (
                 id INTEGER PRIMARY KEY,
-                border MULTIPOLYGON)""")
+                border MULTIPOLYGON)"""
+            )
 
             c.execute(
-                "insert into test_MULTIPOLYGON (id, border)"
-                " VALUES (1, GeomFromText('MULTIPOLYGON(((1 1, 1 -1, -1 -1, -1 1, 1 1)),((1 1, 3 1, 3 3, 1 3, 1 1)))'))"
+                """
+INSERT INTO test_MULTIPOLYGON
+            (id, border)
+VALUES      (1,
+             ST_Geomfromtext(
+'MULTIPOLYGON(((1 1, 1 -1, -1 -1, -1 1, 1 1)),((1 1, 3 1, 3 3, 1 3, 1 1)))'))
+"""
             )
 
-            c.execute("SELECT id, AsText(border) FROM test_MULTIPOLYGON")
+            c.execute("SELECT id, ST_AsText(border) FROM test_MULTIPOLYGON")
             row = c.fetchone()
             self.assertEqual(row[0], 1)
-            self.assertEqual(row[1], 'MULTIPOLYGON(((1 1,1 -1,-1 -1,-1 1,1 1)),((1 1,3 1,3 3,1 3,1 1)))')
+            self.assertEqual(
+                row[1],
+                "MULTIPOLYGON(((1 1,1 -1,-1 -1,-1 1,1 1)),((1 1,3 1,3 3,1 3,1 1)))",
+            )
 
-            c.execute("SELECT id, AsWKB(border) FROM test_MULTIPOLYGON")
+            c.execute("SELECT id, ST_AsWKB(border) FROM test_MULTIPOLYGON")
             row = c.fetchone()
             self.assertEqual(row[0], 1)
             self.assertNotEqual(len(row[1]), 0)
@@ -132,19 +147,21 @@ class test_MySQLdb(capabilities.DatabaseTest):
 
     def test_bug_2671682(self):
         from MySQLdb.constants import ER
+
         try:
-            self.cursor.execute("describe some_non_existent_table");
+            self.cursor.execute("describe some_non_existent_table")
         except self.connection.ProgrammingError as msg:
             self.assertTrue(str(ER.NO_SUCH_TABLE) in str(msg))
 
     def test_bug_3514287(self):
         c = self.cursor
         try:
-            c.execute("""create table bug_3541287 (
+            c.execute(
+                """create table bug_3541287 (
                 c1 CHAR(10),
-                t1 TIMESTAMP)""")
-            c.execute("insert into bug_3541287 (c1,t1) values (%s, NOW())",
-                ("blah",))
+                t1 TIMESTAMP)"""
+            )
+            c.execute("insert into bug_3541287 (c1,t1) values (%s, NOW())", ("blah",))
         finally:
             c.execute("drop table if exists bug_3541287")
 
@@ -165,22 +182,25 @@ class test_MySQLdb(capabilities.DatabaseTest):
         for binary_prefix in (True, False, None):
             kwargs = self.connect_kwargs.copy()
             # needs to be set to can guarantee CHARSET response for normal strings
-            kwargs['charset'] = 'utf8'
-            if binary_prefix != None:
-                kwargs['binary_prefix'] = binary_prefix
+            kwargs["charset"] = "utf8mb4"
+            if binary_prefix is not None:
+                kwargs["binary_prefix"] = binary_prefix
 
             with closing(connection_factory(**kwargs)) as conn:
                 with closing(conn.cursor()) as c:
-                    c.execute('SELECT CHARSET(%s)', (MySQLdb.Binary(b'raw bytes'),))
-                    self.assertEqual(c.fetchall()[0][0], 'binary' if binary_prefix else 'utf8')
+                    c.execute("SELECT CHARSET(%s)", (MySQLdb.Binary(b"raw bytes"),))
+                    self.assertEqual(
+                        c.fetchall()[0][0], "binary" if binary_prefix else "utf8mb4"
+                    )
                     # normal strings should not get prefix
-                    c.execute('SELECT CHARSET(%s)', ('str',))
-                    self.assertEqual(c.fetchall()[0][0], 'utf8')
+                    c.execute("SELECT CHARSET(%s)", ("str",))
+                    self.assertEqual(c.fetchall()[0][0], "utf8mb4")
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     if test_MySQLdb.leak_test:
         import gc
+
         gc.enable()
         gc.set_debug(gc.DEBUG_LEAK)
     unittest.main()
diff --git a/tests/test_MySQLdb_dbapi20.py b/tests/test_MySQLdb_dbapi20.py
index 1e808bd..a0dd92a 100644
--- a/tests/test_MySQLdb_dbapi20.py
+++ b/tests/test_MySQLdb_dbapi20.py
@@ -4,17 +4,22 @@ import unittest
 import MySQLdb
 from configdb import connection_kwargs
 import warnings
+
 warnings.simplefilter("ignore")
 
 
 class test_MySQLdb(dbapi20.DatabaseAPI20Test):
     driver = MySQLdb
     connect_args = ()
-    connect_kw_args = connection_kwargs(dict(sql_mode="ANSI,STRICT_TRANS_TABLES,TRADITIONAL"))
+    connect_kw_args = connection_kwargs(
+        dict(sql_mode="ANSI,STRICT_TRANS_TABLES,TRADITIONAL")
+    )
+
+    def test_setoutputsize(self):
+        pass
 
-    def test_setoutputsize(self): pass
-    def test_setoutputsize_basic(self): pass
-    def test_nextset(self): pass
+    def test_setoutputsize_basic(self):
+        pass
 
     """The tests on fetchone and fetchall and rowcount bogusly
     test for an exception if the statement cannot return a
@@ -36,36 +41,41 @@ class test_MySQLdb(dbapi20.DatabaseAPI20Test):
 
             # cursor.fetchall should raise an Error if called
             # after executing a statement that cannot return rows
-            #self.assertRaises(self.driver.Error,cur.fetchall)
+            # self.assertRaises(self.driver.Error,cur.fetchall)
 
-            cur.execute('select name from %sbooze' % self.table_prefix)
+            cur.execute("select name from %sbooze" % self.table_prefix)
             rows = cur.fetchall()
-            self.assertTrue(cur.rowcount in (-1,len(self.samples)))
-            self.assertEqual(len(rows),len(self.samples),
-                'cursor.fetchall did not retrieve all rows'
-                )
+            self.assertTrue(cur.rowcount in (-1, len(self.samples)))
+            self.assertEqual(
+                len(rows),
+                len(self.samples),
+                "cursor.fetchall did not retrieve all rows",
+            )
             rows = [r[0] for r in rows]
             rows.sort()
-            for i in range(0,len(self.samples)):
-                self.assertEqual(rows[i],self.samples[i],
-                'cursor.fetchall retrieved incorrect rows'
+            for i in range(0, len(self.samples)):
+                self.assertEqual(
+                    rows[i], self.samples[i], "cursor.fetchall retrieved incorrect rows"
                 )
             rows = cur.fetchall()
             self.assertEqual(
-                len(rows),0,
-                'cursor.fetchall should return an empty list if called '
-                'after the whole result set has been fetched'
-                )
-            self.assertTrue(cur.rowcount in (-1,len(self.samples)))
+                len(rows),
+                0,
+                "cursor.fetchall should return an empty list if called "
+                "after the whole result set has been fetched",
+            )
+            self.assertTrue(cur.rowcount in (-1, len(self.samples)))
 
             self.executeDDL2(cur)
-            cur.execute('select name from %sbarflys' % self.table_prefix)
+            cur.execute("select name from %sbarflys" % self.table_prefix)
             rows = cur.fetchall()
-            self.assertTrue(cur.rowcount in (-1,0))
-            self.assertEqual(len(rows),0,
-                'cursor.fetchall should return an empty list if '
-                'a select query returns no rows'
-                )
+            self.assertTrue(cur.rowcount in (-1, 0))
+            self.assertEqual(
+                len(rows),
+                0,
+                "cursor.fetchall should return an empty list if "
+                "a select query returns no rows",
+            )
 
         finally:
             con.close()
@@ -77,39 +87,42 @@ class test_MySQLdb(dbapi20.DatabaseAPI20Test):
 
             # cursor.fetchone should raise an Error if called before
             # executing a select-type query
-            self.assertRaises(self.driver.Error,cur.fetchone)
+            self.assertRaises(self.driver.Error, cur.fetchone)
 
             # cursor.fetchone should raise an Error if called after
             # executing a query that cannot return rows
             self.executeDDL1(cur)
-##             self.assertRaises(self.driver.Error,cur.fetchone)
+            # self.assertRaises(self.driver.Error,cur.fetchone)
 
-            cur.execute('select name from %sbooze' % self.table_prefix)
-            self.assertEqual(cur.fetchone(),None,
-                'cursor.fetchone should return None if a query retrieves '
-                'no rows'
-                )
-            self.assertTrue(cur.rowcount in (-1,0))
+            cur.execute("select name from %sbooze" % self.table_prefix)
+            self.assertEqual(
+                cur.fetchone(),
+                None,
+                "cursor.fetchone should return None if a query retrieves " "no rows",
+            )
+            self.assertTrue(cur.rowcount in (-1, 0))
 
             # cursor.fetchone should raise an Error if called after
             # executing a query that cannot return rows
-            cur.execute("insert into %sbooze values ('Victoria Bitter')" % (
-                self.table_prefix
-                ))
-##             self.assertRaises(self.driver.Error,cur.fetchone)
+            cur.execute(
+                "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix)
+            )
+            # self.assertRaises(self.driver.Error,cur.fetchone)
 
-            cur.execute('select name from %sbooze' % self.table_prefix)
+            cur.execute("select name from %sbooze" % self.table_prefix)
             r = cur.fetchone()
-            self.assertEqual(len(r),1,
-                'cursor.fetchone should have retrieved a single row'
-                )
-            self.assertEqual(r[0],'Victoria Bitter',
-                'cursor.fetchone retrieved incorrect data'
-                )
-##             self.assertEqual(cur.fetchone(),None,
-##                 'cursor.fetchone should return None if no more rows available'
-##                 )
-            self.assertTrue(cur.rowcount in (-1,1))
+            self.assertEqual(
+                len(r), 1, "cursor.fetchone should have retrieved a single row"
+            )
+            self.assertEqual(
+                r[0], "Victoria Bitter", "cursor.fetchone retrieved incorrect data"
+            )
+            # self.assertEqual(
+            #    cur.fetchone(),
+            #    None,
+            #    "cursor.fetchone should return None if no more rows available",
+            # )
+            self.assertTrue(cur.rowcount in (-1, 1))
         finally:
             con.close()
 
@@ -119,81 +132,94 @@ class test_MySQLdb(dbapi20.DatabaseAPI20Test):
         try:
             cur = con.cursor()
             self.executeDDL1(cur)
-##             self.assertEqual(cur.rowcount,-1,
-##                 'cursor.rowcount should be -1 after executing no-result '
-##                 'statements'
-##                 )
-            cur.execute("insert into %sbooze values ('Victoria Bitter')" % (
-                self.table_prefix
-                ))
-##             self.assertTrue(cur.rowcount in (-1,1),
-##                 'cursor.rowcount should == number or rows inserted, or '
-##                 'set to -1 after executing an insert statement'
-##                 )
+            #             self.assertEqual(cur.rowcount,-1,
+            #                 'cursor.rowcount should be -1 after executing no-result '
+            #                 'statements'
+            #                 )
+            cur.execute(
+                "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix)
+            )
+            #             self.assertTrue(cur.rowcount in (-1,1),
+            #                 'cursor.rowcount should == number or rows inserted, or '
+            #                 'set to -1 after executing an insert statement'
+            #                 )
             cur.execute("select name from %sbooze" % self.table_prefix)
-            self.assertTrue(cur.rowcount in (-1,1),
-                'cursor.rowcount should == number of rows returned, or '
-                'set to -1 after executing a select statement'
-                )
+            self.assertTrue(
+                cur.rowcount in (-1, 1),
+                "cursor.rowcount should == number of rows returned, or "
+                "set to -1 after executing a select statement",
+            )
             self.executeDDL2(cur)
-##             self.assertEqual(cur.rowcount,-1,
-##                 'cursor.rowcount not being reset to -1 after executing '
-##                 'no-result statements'
-##                 )
+        #             self.assertEqual(cur.rowcount,-1,
+        #                 'cursor.rowcount not being reset to -1 after executing '
+        #                 'no-result statements'
+        #                 )
         finally:
             con.close()
 
     def test_callproc(self):
-        pass # performed in test_MySQL_capabilities
-
-    def help_nextset_setUp(self,cur):
-        ''' Should create a procedure called deleteme
-            that returns two result sets, first the
-            number of rows in booze then "name from booze"
-        '''
-        sql="""
+        pass  # performed in test_MySQL_capabilities
+
+    def help_nextset_setUp(self, cur):
+        """
+        Should create a procedure called deleteme
+        that returns two result sets, first the
+        number of rows in booze then "name from booze"
+        """
+        sql = """
            create procedure deleteme()
            begin
                select count(*) from %(tp)sbooze;
                select name from %(tp)sbooze;
            end
-        """ % dict(tp=self.table_prefix)
+        """ % dict(
+            tp=self.table_prefix
+        )
         cur.execute(sql)
 
-    def help_nextset_tearDown(self,cur):
-        'If cleaning up is needed after nextSetTest'
+    def help_nextset_tearDown(self, cur):
+        "If cleaning up is needed after nextSetTest"
         cur.execute("drop procedure deleteme")
 
     def test_nextset(self):
-        #from warnings import warn
+        # from warnings import warn
+
         con = self._connect()
         try:
             cur = con.cursor()
-            if not hasattr(cur, 'nextset'):
+            if not hasattr(cur, "nextset"):
                 return
 
             try:
                 self.executeDDL1(cur)
-                sql=self._populate()
+                sql = self._populate()
                 for sql in self._populate():
                     cur.execute(sql)
 
                 self.help_nextset_setUp(cur)
 
-                cur.callproc('deleteme')
-                numberofrows=cur.fetchone()
-                assert numberofrows[0]== len(self.samples)
+                cur.callproc("deleteme")
+                numberofrows = cur.fetchone()
+                assert numberofrows[0] == len(self.samples)
                 assert cur.nextset()
-                names=cur.fetchall()
+                names = cur.fetchall()
                 assert len(names) == len(self.samples)
-                s=cur.nextset()
+                s = cur.nextset()
                 if s:
                     empty = cur.fetchall()
-                    self.assertEqual(len(empty), 0,
-                                     "non-empty result set after other result sets")
-                    #warn("Incompatibility: MySQL returns an empty result set for the CALL itself",
-                    #     Warning)
-                #assert s == None,'No more return sets, should return None'
+                    self.assertEqual(
+                        len(empty), 0, "non-empty result set after other result sets"
+                    )
+                    # warn(
+                    #    ": ".join(
+                    #        [
+                    #            "Incompatibility",
+                    #            "MySQL returns an empty result set for the CALL itself"
+                    #        ]
+                    #    ),
+                    #    Warning,
+                    # )
+                # assert s == None, "No more return sets, should return None"
             finally:
                 self.help_nextset_tearDown(cur)
 
@@ -201,5 +227,5 @@ class test_MySQLdb(dbapi20.DatabaseAPI20Test):
             con.close()
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     unittest.main()
diff --git a/tests/test_MySQLdb_nonstandard.py b/tests/test_MySQLdb_nonstandard.py
index ea05079..c517dad 100644
--- a/tests/test_MySQLdb_nonstandard.py
+++ b/tests/test_MySQLdb_nonstandard.py
@@ -5,6 +5,7 @@ import MySQLdb
 from MySQLdb.constants import FIELD_TYPE
 from configdb import connection_factory
 import warnings
+
 warnings.simplefilter("ignore")
 
 
@@ -36,10 +37,12 @@ class TestCoreModule(unittest.TestCase):
         self.assertTrue(isinstance(_mysql.get_client_info(), str))
 
     def test_escape_string(self):
-        self.assertEqual(_mysql.escape_string(b'foo"bar'),
-                         b'foo\\"bar', "escape byte string")
-        self.assertEqual(_mysql.escape_string(u'foo"bar'),
-                         b'foo\\"bar', "escape unicode string")
+        self.assertEqual(
+            _mysql.escape_string(b'foo"bar'), b'foo\\"bar', "escape byte string"
+        )
+        self.assertEqual(
+            _mysql.escape_string('foo"bar'), b'foo\\"bar', "escape unicode string"
+        )
 
 
 class CoreAPI(unittest.TestCase):
@@ -53,49 +56,61 @@ class CoreAPI(unittest.TestCase):
 
     def test_thread_id(self):
         tid = self.conn.thread_id()
-        self.assertTrue(isinstance(tid, int),
-                        "thread_id didn't return an int.")
+        self.assertTrue(isinstance(tid, int), "thread_id didn't return an int.")
 
-        self.assertRaises(TypeError, self.conn.thread_id, ('evil',),
-                          "thread_id shouldn't accept arguments.")
+        self.assertRaises(
+            TypeError,
+            self.conn.thread_id,
+            ("evil",),
+            "thread_id shouldn't accept arguments.",
+        )
 
     def test_affected_rows(self):
-        self.assertEqual(self.conn.affected_rows(), 0,
-                         "Should return 0 before we do anything.")
-
+        self.assertEqual(
+            self.conn.affected_rows(), 0, "Should return 0 before we do anything."
+        )
 
-    #def test_debug(self):
-        ## FIXME Only actually tests if you lack SUPER
-        #self.assertRaises(MySQLdb.OperationalError,
-                          #self.conn.dump_debug_info)
+    # def test_debug(self):
+    # (FIXME) Only actually tests if you lack SUPER
+    # self.assertRaises(MySQLdb.OperationalError,
+    # self.conn.dump_debug_info)
 
     def test_charset_name(self):
-        self.assertTrue(isinstance(self.conn.character_set_name(), str),
-                        "Should return a string.")
+        self.assertTrue(
+            isinstance(self.conn.character_set_name(), str), "Should return a string."
+        )
 
     def test_host_info(self):
-        self.assertTrue(isinstance(self.conn.get_host_info(), str),
-                        "Should return a string.")
+        self.assertTrue(
+            isinstance(self.conn.get_host_info(), str), "Should return a string."
+        )
 
     def test_proto_info(self):
-        self.assertTrue(isinstance(self.conn.get_proto_info(), int),
-                        "Should return an int.")
+        self.assertTrue(
+            isinstance(self.conn.get_proto_info(), int), "Should return an int."
+        )
 
     def test_server_info(self):
-        self.assertTrue(isinstance(self.conn.get_server_info(), str),
-                        "Should return a string.")
+        self.assertTrue(
+            isinstance(self.conn.get_server_info(), str), "Should return a string."
+        )
 
     def test_client_flag(self):
         conn = connection_factory(
-            use_unicode=True,
-            client_flag=MySQLdb.constants.CLIENT.FOUND_ROWS)
+            use_unicode=True, client_flag=MySQLdb.constants.CLIENT.FOUND_ROWS
+        )
 
-        self.assertIsInstance(conn.client_flag, (int, MySQLdb.compat.long))
+        self.assertIsInstance(conn.client_flag, int)
         self.assertTrue(conn.client_flag & MySQLdb.constants.CLIENT.FOUND_ROWS)
-        with self.assertRaises(TypeError if MySQLdb.compat.PY2 else AttributeError):
+        with self.assertRaises(AttributeError):
             conn.client_flag = 0
 
         conn.close()
 
     def test_fileno(self):
         self.assertGreaterEqual(self.conn.fileno(), 0)
+
+    def test_context_manager(self):
+        with connection_factory() as conn:
+            self.assertFalse(conn.closed)
+        self.assertTrue(conn.closed)
diff --git a/tests/test_MySQLdb_times.py b/tests/test_MySQLdb_times.py
index d9d3e02..2081b1a 100644
--- a/tests/test_MySQLdb_times.py
+++ b/tests/test_MySQLdb_times.py
@@ -1,109 +1,146 @@
-import mock
-import unittest
-from time import gmtime
 from datetime import time, date, datetime, timedelta
+from time import gmtime
+import unittest
+from unittest import mock
+import warnings
 
 from MySQLdb import times
 
-import warnings
+
 warnings.simplefilter("ignore")
 
 
 class TestX_or_None(unittest.TestCase):
     def test_date_or_none(self):
-        assert times.Date_or_None('1969-01-01') == date(1969, 1, 1)
-        assert times.Date_or_None('2015-01-01') == date(2015, 1, 1)
-        assert times.Date_or_None('2015-12-13') == date(2015, 12, 13)
+        assert times.Date_or_None("1969-01-01") == date(1969, 1, 1)
+        assert times.Date_or_None("2015-01-01") == date(2015, 1, 1)
+        assert times.Date_or_None("2015-12-13") == date(2015, 12, 13)
 
-        assert times.Date_or_None('') is None
-        assert times.Date_or_None('fail') is None
-        assert times.Date_or_None('2015-12') is None
-        assert times.Date_or_None('2015-12-40') is None
-        assert times.Date_or_None('0000-00-00') is None
+        assert times.Date_or_None("") is None
+        assert times.Date_or_None("fail") is None
+        assert times.Date_or_None("2015-12") is None
+        assert times.Date_or_None("2015-12-40") is None
+        assert times.Date_or_None("0000-00-00") is None
 
     def test_time_or_none(self):
-        assert times.Time_or_None('00:00:00') == time(0, 0)
-        assert times.Time_or_None('01:02:03') == time(1, 2, 3)
-        assert times.Time_or_None('01:02:03.123456') == time(1, 2, 3, 123456)
+        assert times.Time_or_None("00:00:00") == time(0, 0)
+        assert times.Time_or_None("01:02:03") == time(1, 2, 3)
+        assert times.Time_or_None("01:02:03.123456") == time(1, 2, 3, 123456)
 
-        assert times.Time_or_None('') is None
-        assert times.Time_or_None('fail') is None
-        assert times.Time_or_None('24:00:00') is None
-        assert times.Time_or_None('01:02:03.123456789') is None
+        assert times.Time_or_None("") is None
+        assert times.Time_or_None("fail") is None
+        assert times.Time_or_None("24:00:00") is None
+        assert times.Time_or_None("01:02:03.123456789") is None
 
     def test_datetime_or_none(self):
-        assert times.DateTime_or_None('1000-01-01') == date(1000, 1, 1)
-        assert times.DateTime_or_None('2015-12-13') == date(2015, 12, 13)
-        assert times.DateTime_or_None('2015-12-13 01:02') == datetime(2015, 12, 13, 1, 2)
-        assert times.DateTime_or_None('2015-12-13T01:02') == datetime(2015, 12, 13, 1, 2)
-        assert times.DateTime_or_None('2015-12-13 01:02:03') == datetime(2015, 12, 13, 1, 2, 3)
-        assert times.DateTime_or_None('2015-12-13T01:02:03') == datetime(2015, 12, 13, 1, 2, 3)
-        assert times.DateTime_or_None('2015-12-13 01:02:03.123') == datetime(2015, 12, 13, 1, 2, 3, 123000)
-        assert times.DateTime_or_None('2015-12-13 01:02:03.000123') == datetime(2015, 12, 13, 1, 2, 3, 123)
-        assert times.DateTime_or_None('2015-12-13 01:02:03.123456') == datetime(2015, 12, 13, 1, 2, 3, 123456)
-        assert times.DateTime_or_None('2015-12-13T01:02:03.123456') == datetime(2015, 12, 13, 1, 2, 3, 123456)
-
-        assert times.DateTime_or_None('') is None
-        assert times.DateTime_or_None('fail') is None
-        assert times.DateTime_or_None('0000-00-00 00:00:00') is None
-        assert times.DateTime_or_None('0000-00-00 00:00:00.000000') is None
-        assert times.DateTime_or_None('2015-12-13T01:02:03.123456789') is None
+        assert times.DateTime_or_None("1000-01-01") == date(1000, 1, 1)
+        assert times.DateTime_or_None("2015-12-13") == date(2015, 12, 13)
+        assert times.DateTime_or_None("2015-12-13 01:02") == datetime(
+            2015, 12, 13, 1, 2
+        )
+        assert times.DateTime_or_None("2015-12-13T01:02") == datetime(
+            2015, 12, 13, 1, 2
+        )
+        assert times.DateTime_or_None("2015-12-13 01:02:03") == datetime(
+            2015, 12, 13, 1, 2, 3
+        )
+        assert times.DateTime_or_None("2015-12-13T01:02:03") == datetime(
+            2015, 12, 13, 1, 2, 3
+        )
+        assert times.DateTime_or_None("2015-12-13 01:02:03.123") == datetime(
+            2015, 12, 13, 1, 2, 3, 123000
+        )
+        assert times.DateTime_or_None("2015-12-13 01:02:03.000123") == datetime(
+            2015, 12, 13, 1, 2, 3, 123
+        )
+        assert times.DateTime_or_None("2015-12-13 01:02:03.123456") == datetime(
+            2015, 12, 13, 1, 2, 3, 123456
+        )
+        assert times.DateTime_or_None("2015-12-13T01:02:03.123456") == datetime(
+            2015, 12, 13, 1, 2, 3, 123456
+        )
+
+        assert times.DateTime_or_None("") is None
+        assert times.DateTime_or_None("fail") is None
+        assert times.DateTime_or_None("0000-00-00 00:00:00") is None
+        assert times.DateTime_or_None("0000-00-00 00:00:00.000000") is None
+        assert times.DateTime_or_None("2015-12-13T01:02:03.123456789") is None
 
     def test_timedelta_or_none(self):
-        assert times.TimeDelta_or_None('-1:0:0') == timedelta(0, -3600)
-        assert times.TimeDelta_or_None('1:0:0') == timedelta(0, 3600)
-        assert times.TimeDelta_or_None('12:55:30') == timedelta(0, 46530)
-        assert times.TimeDelta_or_None('12:55:30.123456') == timedelta(0, 46530, 123456)
-        assert times.TimeDelta_or_None('12:55:30.123456789') == timedelta(0, 46653, 456789)
-        assert times.TimeDelta_or_None('12:55:30.123456789123456') == timedelta(1429, 37719, 123456)
-
-        assert times.TimeDelta_or_None('') is None
-        assert times.TimeDelta_or_None('0') is None
-        assert times.TimeDelta_or_None('fail') is None
+        assert times.TimeDelta_or_None("-1:0:0") == timedelta(0, -3600)
+        assert times.TimeDelta_or_None("1:0:0") == timedelta(0, 3600)
+        assert times.TimeDelta_or_None("12:55:30") == timedelta(0, 46530)
+        assert times.TimeDelta_or_None("12:55:30.123456") == timedelta(0, 46530, 123456)
+        assert times.TimeDelta_or_None("12:55:30.123456789") == timedelta(
+            0, 46653, 456789
+        )
+        assert times.TimeDelta_or_None("12:55:30.123456789123456") == timedelta(
+            1429, 37719, 123456
+        )
+
+        assert times.TimeDelta_or_None("") is None
+        assert times.TimeDelta_or_None("0") is None
+        assert times.TimeDelta_or_None("fail") is None
 
 
 class TestTicks(unittest.TestCase):
-    @mock.patch('MySQLdb.times.localtime', side_effect=gmtime)
+    @mock.patch("MySQLdb.times.localtime", side_effect=gmtime)
     def test_date_from_ticks(self, mock):
         assert times.DateFromTicks(0) == date(1970, 1, 1)
         assert times.DateFromTicks(1430000000) == date(2015, 4, 25)
 
-    @mock.patch('MySQLdb.times.localtime', side_effect=gmtime)
+    @mock.patch("MySQLdb.times.localtime", side_effect=gmtime)
     def test_time_from_ticks(self, mock):
         assert times.TimeFromTicks(0) == time(0, 0, 0)
         assert times.TimeFromTicks(1431100000) == time(15, 46, 40)
         assert times.TimeFromTicks(1431100000.123) == time(15, 46, 40)
 
-    @mock.patch('MySQLdb.times.localtime', side_effect=gmtime)
+    @mock.patch("MySQLdb.times.localtime", side_effect=gmtime)
     def test_timestamp_from_ticks(self, mock):
         assert times.TimestampFromTicks(0) == datetime(1970, 1, 1, 0, 0, 0)
         assert times.TimestampFromTicks(1430000000) == datetime(2015, 4, 25, 22, 13, 20)
-        assert times.TimestampFromTicks(1430000000.123) == datetime(2015, 4, 25, 22, 13, 20)
+        assert times.TimestampFromTicks(1430000000.123) == datetime(
+            2015, 4, 25, 22, 13, 20
+        )
 
 
 class TestToLiteral(unittest.TestCase):
     def test_datetime_to_literal(self):
-        assert times.DateTime2literal(datetime(2015, 12, 13), '') == b"'2015-12-13 00:00:00'"
-        assert times.DateTime2literal(datetime(2015, 12, 13, 11, 12, 13), '') == b"'2015-12-13 11:12:13'"
-        assert times.DateTime2literal(datetime(2015, 12, 13, 11, 12, 13, 123456), '') == b"'2015-12-13 11:12:13.123456'"
+        self.assertEqual(
+            times.DateTime2literal(datetime(2015, 12, 13), ""), b"'2015-12-13 00:00:00'"
+        )
+        self.assertEqual(
+            times.DateTime2literal(datetime(2015, 12, 13, 11, 12, 13), ""),
+            b"'2015-12-13 11:12:13'",
+        )
+        self.assertEqual(
+            times.DateTime2literal(datetime(2015, 12, 13, 11, 12, 13, 123456), ""),
+            b"'2015-12-13 11:12:13.123456'",
+        )
 
     def test_datetimedelta_to_literal(self):
         d = datetime(2015, 12, 13, 1, 2, 3) - datetime(2015, 12, 13, 1, 2, 2)
-        assert times.DateTimeDelta2literal(d, '') == b"'0 0:0:1'"
+        assert times.DateTimeDelta2literal(d, "") == b"'0 0:0:1'"
 
 
 class TestFormat(unittest.TestCase):
     def test_format_timedelta(self):
         d = datetime(2015, 1, 1) - datetime(2015, 1, 1)
-        assert times.format_TIMEDELTA(d) == '0 0:0:0'
+        assert times.format_TIMEDELTA(d) == "0 0:0:0"
 
         d = datetime(2015, 1, 1, 10, 11, 12) - datetime(2015, 1, 1, 8, 9, 10)
-        assert times.format_TIMEDELTA(d) == '0 2:2:2'
+        assert times.format_TIMEDELTA(d) == "0 2:2:2"
 
         d = datetime(2015, 1, 1, 10, 11, 12) - datetime(2015, 1, 1, 11, 12, 13)
-        assert times.format_TIMEDELTA(d) == '-1 22:58:59'
+        assert times.format_TIMEDELTA(d) == "-1 22:58:59"
 
     def test_format_timestamp(self):
-        assert times.format_TIMESTAMP(datetime(2015, 2, 3)) == '2015-02-03 00:00:00'
-        assert times.format_TIMESTAMP(datetime(2015, 2, 3, 17, 18, 19)) == '2015-02-03 17:18:19'
-        assert times.format_TIMESTAMP(datetime(15, 2, 3, 17, 18, 19)) == '0015-02-03 17:18:19'
+        assert times.format_TIMESTAMP(datetime(2015, 2, 3)) == "2015-02-03 00:00:00"
+        self.assertEqual(
+            times.format_TIMESTAMP(datetime(2015, 2, 3, 17, 18, 19)),
+            "2015-02-03 17:18:19",
+        )
+        self.assertEqual(
+            times.format_TIMESTAMP(datetime(15, 2, 3, 17, 18, 19)),
+            "0015-02-03 17:18:19",
+        )
diff --git a/tests/test_connection.py b/tests/test_connection.py
new file mode 100644
index 0000000..960de57
--- /dev/null
+++ b/tests/test_connection.py
@@ -0,0 +1,26 @@
+import pytest
+
+from MySQLdb._exceptions import ProgrammingError
+
+from configdb import connection_factory
+
+
+def test_multi_statements_default_true():
+    conn = connection_factory()
+    cursor = conn.cursor()
+
+    cursor.execute("select 17; select 2")
+    rows = cursor.fetchall()
+    assert rows == ((17,),)
+
+
+def test_multi_statements_false():
+    conn = connection_factory(multi_statements=False)
+    cursor = conn.cursor()
+
+    with pytest.raises(ProgrammingError):
+        cursor.execute("select 17; select 2")
+
+    cursor.execute("select 17")
+    rows = cursor.fetchall()
+    assert rows == ((17,),)
diff --git a/tests/test_cursor.py b/tests/test_cursor.py
index ff96368..91f0323 100644
--- a/tests/test_cursor.py
+++ b/tests/test_cursor.py
@@ -1,6 +1,4 @@
-from __future__ import print_function, absolute_import
-
-import pytest
+# import pytest
 import MySQLdb.cursors
 from configdb import connection_factory
 
@@ -8,6 +6,7 @@ from configdb import connection_factory
 _conns = []
 _tables = []
 
+
 def connect(**kwargs):
     conn = connection_factory(**kwargs)
     _conns.append(conn)
@@ -19,7 +18,7 @@ def teardown_function(function):
         c = _conns[0]
         cur = c.cursor()
         for t in _tables:
-            cur.execute("DROP TABLE %s" % (t,))
+            cur.execute("DROP TABLE {}".format(t))
         cur.close()
         del _tables[:]
 
@@ -35,47 +34,71 @@ def test_executemany():
     cursor.execute("create table test (data varchar(10))")
     _tables.append("test")
 
-    m = MySQLdb.cursors.RE_INSERT_VALUES.match("INSERT INTO TEST (ID, NAME) VALUES (%s, %s)")
-    assert m is not None, 'error parse %s'
-    assert m.group(3) == '', 'group 3 not blank, bug in RE_INSERT_VALUES?'
-
-    m = MySQLdb.cursors.RE_INSERT_VALUES.match("INSERT INTO TEST (ID, NAME) VALUES (%(id)s, %(name)s)")
-    assert m is not None, 'error parse %(name)s'
-    assert m.group(3) == '', 'group 3 not blank, bug in RE_INSERT_VALUES?'
-
-    m = MySQLdb.cursors.RE_INSERT_VALUES.match("INSERT INTO TEST (ID, NAME) VALUES (%(id_name)s, %(name)s)")
-    assert m is not None, 'error parse %(id_name)s'
-    assert m.group(3) == '', 'group 3 not blank, bug in RE_INSERT_VALUES?'
-
-    m = MySQLdb.cursors.RE_INSERT_VALUES.match("INSERT INTO TEST (ID, NAME) VALUES (%(id_name)s, %(name)s) ON duplicate update")
-    assert m is not None, 'error parse %(id_name)s'
-    assert m.group(3) == ' ON duplicate update', 'group 3 not ON duplicate update, bug in RE_INSERT_VALUES?'
+    m = MySQLdb.cursors.RE_INSERT_VALUES.match(
+        "INSERT INTO TEST (ID, NAME) VALUES (%s, %s)"
+    )
+    assert m is not None, "error parse %s"
+    assert m.group(3) == "", "group 3 not blank, bug in RE_INSERT_VALUES?"
+
+    m = MySQLdb.cursors.RE_INSERT_VALUES.match(
+        "INSERT INTO TEST (ID, NAME) VALUES (%(id)s, %(name)s)"
+    )
+    assert m is not None, "error parse %(name)s"
+    assert m.group(3) == "", "group 3 not blank, bug in RE_INSERT_VALUES?"
+
+    m = MySQLdb.cursors.RE_INSERT_VALUES.match(
+        "INSERT INTO TEST (ID, NAME) VALUES (%(id_name)s, %(name)s)"
+    )
+    assert m is not None, "error parse %(id_name)s"
+    assert m.group(3) == "", "group 3 not blank, bug in RE_INSERT_VALUES?"
+
+    m = MySQLdb.cursors.RE_INSERT_VALUES.match(
+        "INSERT INTO TEST (ID, NAME) VALUES (%(id_name)s, %(name)s) ON duplicate update"
+    )
+    assert m is not None, "error parse %(id_name)s"
+    assert (
+        m.group(3) == " ON duplicate update"
+    ), "group 3 not ON duplicate update, bug in RE_INSERT_VALUES?"
 
     # https://github.com/PyMySQL/mysqlclient-python/issues/178
-    m = MySQLdb.cursors.RE_INSERT_VALUES.match("INSERT INTO bloup(foo, bar)VALUES(%s, %s)")
+    m = MySQLdb.cursors.RE_INSERT_VALUES.match(
+        "INSERT INTO bloup(foo, bar)VALUES(%s, %s)"
+    )
     assert m is not None
 
-    # cursor._executed myst bee "insert into test (data) values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)"
+    # cursor._executed myst bee
+    # """
+    # insert into test (data)
+    # values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)
+    # """
     # list args
     data = range(10)
     cursor.executemany("insert into test (data) values (%s)", data)
-    assert cursor._executed.endswith(b",(7),(8),(9)"), 'execute many with %s not in one query'
+    assert cursor._executed.endswith(
+        b",(7),(8),(9)"
+    ), "execute many with %s not in one query"
 
     # dict args
-    data_dict = [{'data': i} for i in range(10)]
+    data_dict = [{"data": i} for i in range(10)]
     cursor.executemany("insert into test (data) values (%(data)s)", data_dict)
-    assert cursor._executed.endswith(b",(7),(8),(9)"), 'execute many with %(data)s not in one query'
+    assert cursor._executed.endswith(
+        b",(7),(8),(9)"
+    ), "execute many with %(data)s not in one query"
 
     # %% in column set
-    cursor.execute("""\
+    cursor.execute(
+        """\
         CREATE TABLE percent_test (
             `A%` INTEGER,
-            `B%` INTEGER)""")
+            `B%` INTEGER)"""
+    )
     try:
         q = "INSERT INTO percent_test (`A%%`, `B%%`) VALUES (%s, %s)"
         assert MySQLdb.cursors.RE_INSERT_VALUES.match(q) is not None
         cursor.executemany(q, [(3, 4), (5, 6)])
-        assert cursor._executed.endswith(b"(3, 4),(5, 6)"), "executemany with %% not in one query"
+        assert cursor._executed.endswith(
+            b"(3, 4),(5, 6)"
+        ), "executemany with %% not in one query"
     finally:
         cursor.execute("DROP TABLE IF EXISTS percent_test")
 
@@ -84,7 +107,46 @@ def test_pyparam():
     conn = connect()
     cursor = conn.cursor()
 
-    cursor.execute(u"SELECT %(a)s, %(b)s", {u'a': 1, u'b': 2})
+    cursor.execute("SELECT %(a)s, %(b)s", {"a": 1, "b": 2})
     assert cursor._executed == b"SELECT 1, 2"
-    cursor.execute(b"SELECT %(a)s, %(b)s", {b'a': 3, b'b': 4})
+    cursor.execute(b"SELECT %(a)s, %(b)s", {b"a": 3, b"b": 4})
     assert cursor._executed == b"SELECT 3, 4"
+
+
+def test_dictcursor():
+    conn = connect()
+    cursor = conn.cursor(MySQLdb.cursors.DictCursor)
+
+    cursor.execute("CREATE TABLE t1 (a int, b int, c int)")
+    _tables.append("t1")
+    cursor.execute("INSERT INTO t1 (a,b,c) VALUES (1,1,47), (2,2,47)")
+
+    cursor.execute("CREATE TABLE t2 (b int, c int)")
+    _tables.append("t2")
+    cursor.execute("INSERT INTO t2 (b,c) VALUES (1,1), (2,2)")
+
+    cursor.execute("SELECT * FROM t1 JOIN t2 ON t1.b=t2.b")
+    rows = cursor.fetchall()
+
+    assert len(rows) == 2
+    assert rows[0] == {"a": 1, "b": 1, "c": 47, "t2.b": 1, "t2.c": 1}
+    assert rows[1] == {"a": 2, "b": 2, "c": 47, "t2.b": 2, "t2.c": 2}
+
+    names1 = sorted(rows[0])
+    names2 = sorted(rows[1])
+    for a, b in zip(names1, names2):
+        assert a is b
+
+    # Old fetchtype
+    cursor._fetch_type = 2
+    cursor.execute("SELECT * FROM t1 JOIN t2 ON t1.b=t2.b")
+    rows = cursor.fetchall()
+
+    assert len(rows) == 2
+    assert rows[0] == {"t1.a": 1, "t1.b": 1, "t1.c": 47, "t2.b": 1, "t2.c": 1}
+    assert rows[1] == {"t1.a": 2, "t1.b": 2, "t1.c": 47, "t2.b": 2, "t2.c": 2}
+
+    names1 = sorted(rows[0])
+    names2 = sorted(rows[1])
+    for a, b in zip(names1, names2):
+        assert a is b

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/lib/debug/.build-id/2a/4bbdd9cb865571a55e29b679cec6ce14812964.debug
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/MySQLdb/_mysql.cpython-310-x86_64-linux-gnu.so
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/mysqlclient-2.1.1.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/mysqlclient-2.1.1.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/mysqlclient-2.1.1.egg-info/top_level.txt

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/lib/debug/.build-id/4d/57edf3da3ac7bec2e44e8242203231c45f1b38.debug
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/MySQLdb/_mysql.cpython-311-x86_64-linux-gnu.so
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/MySQLdb/compat.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/mysqlclient-1.4.6.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/mysqlclient-1.4.6.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/mysqlclient-1.4.6.egg-info/top_level.txt

Control files of package python3-mysqldb: lines which differ (wdiff format)

  • Depends: python3 (<< 3.12), 3.11), python3 (>= 3.11~), 3.10~), python3:any, libc6 (>= 2.4), libmariadb3 (>= 3.0.0)

Control files of package python3-mysqldb-dbgsym: lines which differ (wdiff format)

  • Build-Ids: 4d57edf3da3ac7bec2e44e8242203231c45f1b38 2a4bbdd9cb865571a55e29b679cec6ce14812964

More details

Full run details