From 684dcbf0657f18c1ba12fe21732323c890ff20ab Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 22 Jun 2022 18:07:11 +0900 Subject: [PATCH 01/39] Actions: Drop Python 3.6 and add 3.11-dev (#542) --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index e3c0fec1..1341e0f4 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11-dev"] steps: - name: Start MySQL run: | From dac24e7b05b83eb1511e25e3cd8f8c20b7fbe112 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 22 Jun 2022 18:08:05 +0900 Subject: [PATCH 02/39] Update metadata --- metadata.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metadata.cfg b/metadata.cfg index 5bf1e815..0d35d8fc 100644 --- a/metadata.cfg +++ b/metadata.cfg @@ -20,11 +20,11 @@ classifiers: Programming Language :: C Programming Language :: Python Programming Language :: Python :: 3 - 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 + Programming Language :: Python :: 3.11 Topic :: Database Topic :: Database :: Database Engines/Servers py_modules: From f5a2f3dd280679bd6e33d3f44d2121ae485e956f Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Fri, 5 Aug 2022 12:40:27 +1000 Subject: [PATCH 03/39] docs: fix simple typo, portible -> portable (#547) There is a small typo in tests/dbapi20.py. Should read `portable` rather than `portible`. --- tests/dbapi20.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/dbapi20.py b/tests/dbapi20.py index 4965c9bf..a88a0616 100644 --- a/tests/dbapi20.py +++ b/tests/dbapi20.py @@ -56,7 +56,7 @@ # - self.populate is now self._populate(), so if a driver stub # overrides self.ddl1 this change propagates # - VARCHAR columns now have a width, which will hopefully make the -# DDL even more portible (this will be reversed if it causes more problems) +# DDL even more portable (this will be reversed if it causes more problems) # - cursor.rowcount being checked after various execute and fetchXXX methods # - Check for fetchall and fetchmany returning empty lists after results # are exhausted (already checking for empty lists if select retrieved From d288d3e224e68a6e1736282d09ec74a50c227530 Mon Sep 17 00:00:00 2001 From: gopackgo90 Date: Mon, 19 Sep 2022 02:23:55 -0500 Subject: [PATCH 04/39] Update python_requires to 3.7+ (#543) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index dfa661c1..aa6c34fb 100644 --- a/setup.py +++ b/setup.py @@ -18,5 +18,5 @@ ] metadata["long_description"] = readme metadata["long_description_content_type"] = "text/markdown" -metadata["python_requires"] = ">=3.5" +metadata["python_requires"] = ">=3.7" setuptools.setup(**metadata) From 8f0cbacba853f0328b839b742b32a0ebb0687b8e Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 20 Sep 2022 14:10:19 +0900 Subject: [PATCH 05/39] Raise ProgrammingError on -inf (#557) --- MySQLdb/converters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MySQLdb/converters.py b/MySQLdb/converters.py index 33f22f74..d6fdc01c 100644 --- a/MySQLdb/converters.py +++ b/MySQLdb/converters.py @@ -72,7 +72,7 @@ def Thing2Str(s, d): def Float2Str(o, d): s = repr(o) - if s in ("inf", "nan"): + if s in ("inf", "-inf", "nan"): raise ProgrammingError("%s can not be used with MySQL" % s) if "e" not in s: s += "e0" From 6f5fca04fe58c7ddd41c2a64b9c00cb0e167f4b3 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 28 Oct 2022 01:08:39 -0500 Subject: [PATCH 06/39] swap 3.11-dev for 3.11 in CI (#561) --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 1341e0f4..0b34ecb4 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11-dev"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - name: Start MySQL run: | From 0914c8cd778f27fccce6f3895d80bfaa64520d47 Mon Sep 17 00:00:00 2001 From: "lgtm-com[bot]" <43144390+lgtm-com[bot]@users.noreply.github.com> Date: Thu, 10 Nov 2022 15:09:40 +0900 Subject: [PATCH 07/39] Add CodeQL workflow for GitHub code scanning (#565) Co-authored-by: LGTM Migrator --- .github/workflows/codeql.yml | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..90a8e5b0 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,42 @@ +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: "29 15 * * 6" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ python, cpp ] + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + queries: +security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + if: ${{ matrix.language == 'python' || matrix.language == 'cpp' }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{ matrix.language }}" From da4c072431a2eaf533226139a00b3aa43fbd3d6c Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 17 Jan 2023 17:23:07 +0900 Subject: [PATCH 08/39] Add .readthedocs.yaml --- .readthedocs.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..1a7c18e8 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,9 @@ +version: 2 + +python: + version: 3.9 + +build: + apt_packages: + - default-libmysqlclient-dev + - build-essential From 589740a67a2e82fe80489e984d625cf84a36c875 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 17 Jan 2023 17:24:49 +0900 Subject: [PATCH 09/39] RTD: Use Python 3.8 --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 1a7c18e8..26440026 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,7 +1,7 @@ version: 2 python: - version: 3.9 + version: 3.8 build: apt_packages: From cc1b042d8a0fafbbaa389cb1fad1704a498051d3 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 17 Jan 2023 17:26:59 +0900 Subject: [PATCH 10/39] RTD: Use ubuntu-22.04 --- .readthedocs.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 26440026..ec91283e 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,7 +1,9 @@ version: 2 -python: - version: 3.8 +build: + os: ubuntu-22.04 + tools: + python: "3.11" build: apt_packages: From b419beab9e60dd5053a8de2864e0c668bb1a6caf Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 17 Jan 2023 17:33:50 +0900 Subject: [PATCH 11/39] RTD: Fix yaml --- .readthedocs.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index ec91283e..eaffaf39 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -4,8 +4,6 @@ build: os: ubuntu-22.04 tools: python: "3.11" - -build: apt_packages: - default-libmysqlclient-dev - build-essential From 58465cfa8704fc51fe3173254d7b822abb30e575 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Mon, 13 Mar 2023 19:09:17 +0900 Subject: [PATCH 12/39] ER_BAD_NULL should be IntegrityError. (#579) Fixes #535 --- MySQLdb/_mysql.c | 1 + tests/capabilities.py | 3 -- tests/test_MySQLdb_capabilities.py | 1 - tests/test_errors.py | 56 ++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 tests/test_errors.py diff --git a/MySQLdb/_mysql.c b/MySQLdb/_mysql.c index 7737dbe7..6c04ec99 100644 --- a/MySQLdb/_mysql.c +++ b/MySQLdb/_mysql.c @@ -180,6 +180,7 @@ _mysql_Exception(_mysql_ConnectionObject *c) #ifdef ER_NO_DEFAULT_FOR_FIELD case ER_NO_DEFAULT_FOR_FIELD: #endif + case ER_BAD_NULL_ERROR: e = _mysql_IntegrityError; break; #ifdef ER_WARNING_NOT_COMPLETE_ROLLBACK diff --git a/tests/capabilities.py b/tests/capabilities.py index da753d15..034e88da 100644 --- a/tests/capabilities.py +++ b/tests/capabilities.py @@ -11,7 +11,6 @@ class DatabaseTest(unittest.TestCase): - db_module = None connect_args = () connect_kwargs = dict() @@ -20,7 +19,6 @@ class DatabaseTest(unittest.TestCase): debug = False def setUp(self): - db = connection_factory(**self.connect_kwargs) self.connection = db self.cursor = db.cursor() @@ -67,7 +65,6 @@ def new_table_name(self): i = i + 1 def create_table(self, columndefs): - """Create a table using a list of column definitions given in columndefs. diff --git a/tests/test_MySQLdb_capabilities.py b/tests/test_MySQLdb_capabilities.py index fc213b84..dbff27c2 100644 --- a/tests/test_MySQLdb_capabilities.py +++ b/tests/test_MySQLdb_capabilities.py @@ -12,7 +12,6 @@ class test_MySQLdb(capabilities.DatabaseTest): - db_module = MySQLdb connect_args = () connect_kwargs = dict( diff --git a/tests/test_errors.py b/tests/test_errors.py new file mode 100644 index 00000000..3a9fecaa --- /dev/null +++ b/tests/test_errors.py @@ -0,0 +1,56 @@ +import pytest +import MySQLdb.cursors +from configdb import connection_factory + + +_conns = [] +_tables = [] + + +def connect(**kwargs): + conn = connection_factory(**kwargs) + _conns.append(conn) + return conn + + +def teardown_function(function): + if _tables: + c = _conns[0] + cur = c.cursor() + for t in _tables: + cur.execute("DROP TABLE {}".format(t)) + cur.close() + del _tables[:] + + for c in _conns: + c.close() + del _conns[:] + + +def test_null(): + """Inserting NULL into non NULLABLE column""" + # https://github.com/PyMySQL/mysqlclient/issues/535 + table_name = "test_null" + conn = connect() + cursor = conn.cursor() + + cursor.execute(f"create table {table_name} (c1 int primary key)") + _tables.append(table_name) + + with pytest.raises(MySQLdb.IntegrityError): + cursor.execute(f"insert into {table_name} values (null)") + + +def test_duplicated_pk(): + """Inserting row with duplicated PK""" + # https://github.com/PyMySQL/mysqlclient/issues/535 + table_name = "test_duplicated_pk" + conn = connect() + cursor = conn.cursor() + + cursor.execute(f"create table {table_name} (c1 int primary key)") + _tables.append(table_name) + + cursor.execute(f"insert into {table_name} values (1)") + with pytest.raises(MySQLdb.IntegrityError): + cursor.execute(f"insert into {table_name} values (1)") From 17c4e466d9b752c4a71362ef5b0c4b8681de4362 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Sat, 1 Apr 2023 00:01:00 +0900 Subject: [PATCH 13/39] Update security policy. --- SECURITY.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index a54d21b1..75f0c541 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,9 +1,5 @@ -# Security Policy +## Security contact information -## Supported Versions - -2.1.x - -## Reporting a Vulnerability - -email: songofacandy@gmail.com +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. \ No newline at end of file From d0658273acc9e8b929d7e8885487da41ef6461cc Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Sun, 7 May 2023 10:33:42 +0900 Subject: [PATCH 14/39] Update windows build workflow (#585) Use MariaDB Connector/C 3.3.4 --- .github/workflows/windows.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/windows.yaml b/.github/workflows/windows.yaml index e2314b44..bd711075 100644 --- a/.github/workflows/windows.yaml +++ b/.github/workflows/windows.yaml @@ -11,12 +11,12 @@ jobs: build: runs-on: windows-latest env: - CONNECTOR_VERSION: "3.3.1" + CONNECTOR_VERSION: "3.3.4" steps: - name: Cache Connector id: cache-connector - uses: actions/cache@v1 + uses: actions/cache@v3 with: path: c:/mariadb-connector key: mariadb-connector-c-${{ env.CONNECTOR_VERSION }}-win @@ -41,7 +41,7 @@ jobs: cmake -DCMAKE_INSTALL_PREFIX=c:/mariadb-connector -DCMAKE_INSTALL_COMPONENT=Development -DCMAKE_BUILD_TYPE=Release -P cmake_install.cmake - name: Checkout mysqlclient - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: mysqlclient @@ -58,9 +58,9 @@ jobs: EOF cat site.cfg - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 - name: Install cibuildwheel - run: python -m pip install cibuildwheel==2.7.0 + run: python -m pip install cibuildwheel==2.12.3 - name: Build wheels working-directory: mysqlclient env: @@ -70,7 +70,7 @@ jobs: run: "python -m cibuildwheel --prerelease-pythons --output-dir dist" - name: Upload Wheel - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: win-wheels path: mysqlclient/dist/*.whl From 14538b2ccde0b1287d019ddf674c27e71213c735 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 9 May 2023 10:36:25 +0900 Subject: [PATCH 15/39] Use pkg-config instead of mysql_config (#586) MySQL breaks mysql_config often. Use pkg-config instead. Fixes #584 --- README.md | 2 +- metadata.cfg | 5 +- setup_common.py | 4 +- setup_posix.py | 178 ++++++++++++++--------------------------------- setup_windows.py | 14 ++-- site.cfg | 5 -- 6 files changed, 65 insertions(+), 143 deletions(-) diff --git a/README.md b/README.md index 4dbc54d7..23db1f27 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ $ pip install mysqlclient ### Customize build (POSIX) -mysqlclient uses `mysql_config` or `mariadb_config` by default for finding +mysqlclient uses `pkg-config --clfags --ldflags mysqlclient` by default for finding compiler/linker flags. You can use `MYSQLCLIENT_CFLAGS` and `MYSQLCLIENT_LDFLAGS` environment diff --git a/metadata.cfg b/metadata.cfg index 0d35d8fc..4d5e0174 100644 --- a/metadata.cfg +++ b/metadata.cfg @@ -1,6 +1,7 @@ [metadata] -version: 2.1.1 -version_info: (2,1,1,'final',0) +name: mysqlclient +version: 2.1.2 +version_info: (2,1,2,'dev',0) description: Python interface to MySQL author: Inada Naoki author_email: songofacandy@gmail.com diff --git a/setup_common.py b/setup_common.py index 5b6927ac..8d7a37d5 100644 --- a/setup_common.py +++ b/setup_common.py @@ -1,8 +1,8 @@ -from configparser import ConfigParser as SafeConfigParser +from configparser import ConfigParser def get_metadata_and_options(): - config = SafeConfigParser() + config = ConfigParser() config.read(["metadata.cfg", "site.cfg"]) metadata = dict(config.items("metadata")) diff --git a/setup_posix.py b/setup_posix.py index 99763cbc..a03dd22c 100644 --- a/setup_posix.py +++ b/setup_posix.py @@ -1,61 +1,29 @@ import os import sys +import subprocess -# 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 ?" - ) - if s[0] in "\"'" and s[0] == s[-1]: - s = s[1:-1] - return s - - -_mysql_config_path = "mysql_config" - - -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: - data = [] - if ret / 256 > 1: - raise OSError("{} not found".format(_mysql_config_path)) - print(data) - return data +def find_package_name(): + """Get available pkg-config package name""" + packages = ["mysqlclient", "mariadb"] + for pkg in packages: + try: + cmd = f"pkg-config --exists {pkg}" + print(f"Trying {cmd}") + subprocess.check_call(cmd, shell=True) + except subprocess.CalledProcessError as err: + print(err) + else: + return pkg + raise Exception("Can not find valid pkg-config") 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"] - else: - try: - mysql_config("version") - except OSError: - # try mariadb_config - _mysql_config_path = "mariadb_config" - try: - mysql_config("version") - except OSError: - _mysql_config_path = "mysql_config" - - extra_objects = [] 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 # @@ -63,106 +31,64 @@ def get_config(): static = True 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"))] - + ldflags = os.environ.get("MYSQLCLIENT_LDFLAGS") 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") - - include_dirs = [] - extra_compile_args = ["-std=c99"] - 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 - 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 + pkg_name = None + static_opt = " --static" if static else "" + if not (cflags and ldflags): + pkg_name = find_package_name() + if not cflags: + cflags = subprocess.check_output( + f"pkg-config{static_opt} --cflags {pkg_name}", encoding="utf-8", shell=True + ) + if not ldflags: + ldflags = subprocess.check_output( + f"pkg-config{static_opt} --libs {pkg_name}", encoding="utf-8", shell=True + ) - 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", - ] - for c in CLIENT_LIST: - if c in libraries: - client = c - break - - 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)) - if client in libraries: - libraries.remove(client) + cflags = cflags.split() + for f in cflags: + if f.startswith("-std="): + break 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) + cflags += ["-std=c99"] - name = "mysqlclient" - metadata["name"] = name + ldflags = ldflags.split() define_macros = [ ("version_info", metadata["version_info"]), ("__version__", metadata["version"]), ] - create_release_file(metadata) - del metadata["version_info"] + + # print(f"{cflags = }") + # print(f"{ldflags = }") + # print(f"{define_macros = }") + 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, + extra_compile_args=cflags, + extra_link_args=ldflags, define_macros=define_macros, ) - # newer versions of gcc require libstdc++ if doing a static build if static: ext_options["language"] = "c++" - print("ext_options:") + print("Options for building extention module:") for k, v in ext_options.items(): - print(" {}: {}".format(k, v)) + print(f" {k}: {v}") + + create_release_file(metadata) + del metadata["version_info"] return metadata, ext_options if __name__ == "__main__": - sys.stderr.write( - """You shouldn't be running this directly; it is used by setup.py.""" - ) + from pprint import pprint + + metadata, config = get_config() + print("# Metadata") + pprint(metadata, sort_dicts=False, compact=True) + print("\n# Extention options") + pprint(config, sort_dicts=False, compact=True) diff --git a/setup_windows.py b/setup_windows.py index b2feb7d2..5d8d7158 100644 --- a/setup_windows.py +++ b/setup_windows.py @@ -1,5 +1,4 @@ import os -import sys def get_config(): @@ -38,9 +37,6 @@ def get_config(): extra_link_args = ["/MANIFEST"] - name = "mysqlclient" - metadata["name"] = name - define_macros = [ ("version_info", metadata["version_info"]), ("__version__", metadata["version"]), @@ -59,6 +55,10 @@ def get_config(): if __name__ == "__main__": - sys.stderr.write( - """You shouldn't be running this directly; it is used by setup.py.""" - ) + from pprint import pprint + + metadata, config = get_config() + print("# Metadata") + pprint(metadata) + print("\n# Extention options") + pprint(config) diff --git a/site.cfg b/site.cfg index 08a14b0e..39e3c2b1 100644 --- a/site.cfg +++ b/site.cfg @@ -2,11 +2,6 @@ # static: link against a static library static = False -# The path to mysql_config. -# Only use this if mysql_config is not on your PATH, or you have some weird -# setup that requires it. -#mysql_config = /usr/local/bin/mysql_config - # 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 = From aed1dd26327d9a5baeeb5704c14c8b5fc8f9f5d8 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 9 May 2023 11:32:48 +0900 Subject: [PATCH 16/39] Remove uneeded code. (#512) --- MySQLdb/connections.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/MySQLdb/connections.py b/MySQLdb/connections.py index 38324665..f71a55cd 100644 --- a/MySQLdb/connections.py +++ b/MySQLdb/connections.py @@ -144,7 +144,6 @@ class object, used to create cursors (keyword only) """ from MySQLdb.constants import CLIENT, FIELD_TYPE from MySQLdb.converters import conversions, _bytes_or_str - from weakref import proxy kwargs2 = kwargs.copy() @@ -214,13 +213,6 @@ class object, used to create cursors (keyword only) # MySQL may return JSON with charset==binary. 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._transactional = self.server_capabilities & CLIENT.TRANSACTIONS if self._transactional: if autocommit is not None: From df52e237b3de45646e24ac542c268211cc2b80a8 Mon Sep 17 00:00:00 2001 From: Vince Salvino Date: Mon, 8 May 2023 22:45:28 -0400 Subject: [PATCH 17/39] Add collation option (#564) Fixes #563 --- MySQLdb/connections.py | 16 ++++++++++++++-- doc/user_guide.rst | 16 ++++++++++++++++ tests/test_MySQLdb_nonstandard.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/MySQLdb/connections.py b/MySQLdb/connections.py index f71a55cd..f56c4f54 100644 --- a/MySQLdb/connections.py +++ b/MySQLdb/connections.py @@ -97,6 +97,14 @@ class object, used to create cursors (keyword only) If supplied, the connection character set will be changed to this character set. + :param str collation: + If ``charset`` and ``collation`` are both supplied, the + character set and collation for the current connection + will be set. + + If omitted, empty string, or None, the default collation + for the ``charset`` is implied. + :param str auth_plugin: If supplied, the connection default authentication plugin will be changed to this value. Example values: @@ -167,6 +175,7 @@ class object, used to create cursors (keyword only) cursorclass = kwargs2.pop("cursorclass", self.default_cursor) charset = kwargs2.get("charset", "") + collation = kwargs2.pop("collation", "") use_unicode = kwargs2.pop("use_unicode", True) sql_mode = kwargs2.pop("sql_mode", "") self._binary_prefix = kwargs2.pop("binary_prefix", False) @@ -193,7 +202,7 @@ class object, used to create cursors (keyword only) if not charset: charset = self.character_set_name() - self.set_character_set(charset) + self.set_character_set(charset, collation) if sql_mode: self.set_sql_mode(sql_mode) @@ -285,10 +294,13 @@ def begin(self): """ self.query(b"BEGIN") - def set_character_set(self, charset): + def set_character_set(self, charset, collation=None): """Set the connection character set to charset.""" super().set_character_set(charset) self.encoding = _charset_to_encoding.get(charset, charset) + if collation: + self.query("SET NAMES %s COLLATE %s" % (charset, collation)) + self.store_result() def set_sql_mode(self, sql_mode): """Set the connection sql_mode. See MySQL documentation for diff --git a/doc/user_guide.rst b/doc/user_guide.rst index 555adf15..5c9577bc 100644 --- a/doc/user_guide.rst +++ b/doc/user_guide.rst @@ -348,6 +348,22 @@ connect(parameters...) *This must be a keyword parameter.* + collation + If ``charset`` and ``collation`` are both supplied, the + character set and collation for the current connection + will be set. + + If omitted, empty string, or None, the default collation + for the ``charset`` is implied by the database server. + + To learn more about the quiddities of character sets and + collations, consult the `MySQL docs + `_ + and `MariaDB docs + `_ + + *This must be a keyword parameter.* + sql_mode If present, the session SQL mode will be set to the given string. For more information on sql_mode, see the MySQL diff --git a/tests/test_MySQLdb_nonstandard.py b/tests/test_MySQLdb_nonstandard.py index c517dad3..5e841791 100644 --- a/tests/test_MySQLdb_nonstandard.py +++ b/tests/test_MySQLdb_nonstandard.py @@ -114,3 +114,33 @@ def test_context_manager(self): with connection_factory() as conn: self.assertFalse(conn.closed) self.assertTrue(conn.closed) + + +class TestCollation(unittest.TestCase): + """Test charset and collation connection options.""" + + def setUp(self): + # Initialize a connection with a non-default character set and + # collation. + self.conn = connection_factory( + charset="utf8mb4", + collation="utf8mb4_esperanto_ci", + ) + + def tearDown(self): + self.conn.close() + + def test_charset_collation(self): + c = self.conn.cursor() + c.execute( + """ + SHOW VARIABLES WHERE + Variable_Name="character_set_connection" OR + Variable_Name="collation_connection"; + """ + ) + row = c.fetchall() + charset = row[0][1] + collation = row[1][1] + self.assertEqual(charset, "utf8mb4") + self.assertEqual(collation, "utf8mb4_esperanto_ci") From c56fc43482a09ec6bb5e21d20baf5a86f89156f5 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 9 May 2023 11:45:45 +0900 Subject: [PATCH 18/39] Start 2.2.0 development (#587) --- HISTORY.rst | 10 ++++++++++ metadata.cfg | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 674c6881..13e5cb01 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,3 +1,13 @@ +====================== + What's new in 2.2.0 +====================== + +Release: TBD + +* Use ``pkg-config`` instead of ``mysql_config`` (#586) + + + ====================== What's new in 2.1.1 ====================== diff --git a/metadata.cfg b/metadata.cfg index 4d5e0174..87ebc6c5 100644 --- a/metadata.cfg +++ b/metadata.cfg @@ -1,7 +1,7 @@ [metadata] name: mysqlclient -version: 2.1.2 -version_info: (2,1,2,'dev',0) +version: 2.2.0dev0 +version_info: (2,2,0,'dev',0) description: Python interface to MySQL author: Inada Naoki author_email: songofacandy@gmail.com From 418b68dc5f5d677b595944f71ccf36d12753f7a1 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 9 May 2023 13:48:58 +0900 Subject: [PATCH 19/39] Action: Use Ruff (#588) --- .github/workflows/lint.yaml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index d6aff95a..77a13c22 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -4,14 +4,8 @@ on: [push, pull_request] jobs: lint: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 - uses: psf/black@stable - - name: Setup flake8 annotations - uses: rbialon/flake8-annotations@v1 - - name: flake8 - run: | - pip install flake8 - flake8 --ignore=E203,E501,W503 --max-line-length=88 . + - uses: chartboost/ruff-action@v1 From 1f906e66c4082c305645ddcded18018cecc302fd Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 9 May 2023 14:10:53 +0900 Subject: [PATCH 20/39] Run pyupgrade --py38-plus (#590) --- MySQLdb/connections.py | 2 +- MySQLdb/constants/CR.py | 2 +- MySQLdb/constants/ER.py | 2 +- setup_common.py | 2 +- tests/capabilities.py | 14 +++++++------- tests/dbapi20.py | 3 +-- tests/test_cursor.py | 2 +- tests/test_errors.py | 2 +- 8 files changed, 14 insertions(+), 15 deletions(-) diff --git a/MySQLdb/connections.py b/MySQLdb/connections.py index f56c4f54..865d129a 100644 --- a/MySQLdb/connections.py +++ b/MySQLdb/connections.py @@ -299,7 +299,7 @@ def set_character_set(self, charset, collation=None): super().set_character_set(charset) self.encoding = _charset_to_encoding.get(charset, charset) if collation: - self.query("SET NAMES %s COLLATE %s" % (charset, collation)) + self.query(f"SET NAMES {charset} COLLATE {collation}") self.store_result() def set_sql_mode(self, sql_mode): diff --git a/MySQLdb/constants/CR.py b/MySQLdb/constants/CR.py index 9d33cf65..9467ae11 100644 --- a/MySQLdb/constants/CR.py +++ b/MySQLdb/constants/CR.py @@ -29,7 +29,7 @@ data[value].add(name) for value, names in sorted(data.items()): for name in sorted(names): - print("{} = {}".format(name, value)) + print(f"{name} = {value}") if error_last is not None: print("ERROR_LAST = %s" % error_last) diff --git a/MySQLdb/constants/ER.py b/MySQLdb/constants/ER.py index fcd5bf2e..8c5ece24 100644 --- a/MySQLdb/constants/ER.py +++ b/MySQLdb/constants/ER.py @@ -30,7 +30,7 @@ data[value].add(name) for value, names in sorted(data.items()): for name in sorted(names): - print("{} = {}".format(name, value)) + print(f"{name} = {value}") if error_last is not None: print("ERROR_LAST = %s" % error_last) diff --git a/setup_common.py b/setup_common.py index 8d7a37d5..53869aa2 100644 --- a/setup_common.py +++ b/setup_common.py @@ -22,7 +22,7 @@ def enabled(options, option): elif s in ("no", "false", "0", "n"): return False else: - raise ValueError("Unknown value {} for option {}".format(value, option)) + raise ValueError(f"Unknown value {value} for option {option}") def create_release_file(metadata): diff --git a/tests/capabilities.py b/tests/capabilities.py index 034e88da..1e695e9e 100644 --- a/tests/capabilities.py +++ b/tests/capabilities.py @@ -35,13 +35,13 @@ def tearDown(self): del self.cursor orphans = gc.collect() - self.failIf( + self.assertFalse( orphans, "%d orphaned objects found after deleting cursor" % orphans ) del self.connection orphans = gc.collect() - self.failIf( + self.assertFalse( orphans, "%d orphaned objects found after deleting connection" % orphans ) @@ -82,7 +82,7 @@ def create_table(self, columndefs): def check_data_integrity(self, columndefs, generator): # insert self.create_table(columndefs) - insert_statement = "INSERT INTO %s VALUES (%s)" % ( + insert_statement = "INSERT INTO {} VALUES ({})".format( self.table, ",".join(["%s"] * len(columndefs)), ) @@ -113,7 +113,7 @@ def generator(row, col): return ("%i" % (row % 10)) * 255 self.create_table(columndefs) - insert_statement = "INSERT INTO %s VALUES (%s)" % ( + insert_statement = "INSERT INTO {} VALUES ({})".format( self.table, ",".join(["%s"] * len(columndefs)), ) @@ -131,11 +131,11 @@ def generator(row, col): 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)) + self.cursor.execute(f"select col1 from {self.table} where col1=%s", (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)) + self.cursor.execute(f"select col1 from {self.table} where col1=%s", (0,)) res = self.cursor.fetchall() self.assertTrue(len(res) == 1, "ROLLBACK didn't work") self.cursor.execute("drop table %s" % (self.table)) @@ -150,7 +150,7 @@ def generator(row, col): return ("%i" % (row % 10)) * ((255 - self.rows // 2) + row) self.create_table(columndefs) - insert_statement = "INSERT INTO %s VALUES (%s)" % ( + insert_statement = "INSERT INTO {} VALUES ({})".format( self.table, ",".join(["%s"] * len(columndefs)), ) diff --git a/tests/dbapi20.py b/tests/dbapi20.py index a88a0616..be0f6292 100644 --- a/tests/dbapi20.py +++ b/tests/dbapi20.py @@ -525,8 +525,7 @@ def _populate(self): tests. """ populate = [ - "insert into {}booze values ('{}')".format(self.table_prefix, s) - for s in self.samples + f"insert into {self.table_prefix}booze values ('{s}')" for s in self.samples ] return populate diff --git a/tests/test_cursor.py b/tests/test_cursor.py index 91f0323e..80e21888 100644 --- a/tests/test_cursor.py +++ b/tests/test_cursor.py @@ -18,7 +18,7 @@ def teardown_function(function): c = _conns[0] cur = c.cursor() for t in _tables: - cur.execute("DROP TABLE {}".format(t)) + cur.execute(f"DROP TABLE {t}") cur.close() del _tables[:] diff --git a/tests/test_errors.py b/tests/test_errors.py index 3a9fecaa..fae28e81 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -18,7 +18,7 @@ def teardown_function(function): c = _conns[0] cur = c.cursor() for t in _tables: - cur.execute("DROP TABLE {}".format(t)) + cur.execute(f"DROP TABLE {t}") cur.close() del _tables[:] From d2c07e8a0e760025edef2b753fffacd2b75b75d0 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 9 May 2023 14:40:56 +0900 Subject: [PATCH 21/39] Update workflows (#593) * Drop Python 3.7 * Use latest actions Fix #591 --- .github/workflows/django.yaml | 17 ++++------------- .github/workflows/tests.yaml | 21 ++++++--------------- metadata.cfg | 1 - requirements.txt | 5 +++++ 4 files changed, 15 insertions(+), 29 deletions(-) create mode 100644 requirements.txt diff --git a/.github/workflows/django.yaml b/.github/workflows/django.yaml index 4e18374a..55497767 100644 --- a/.github/workflows/django.yaml +++ b/.github/workflows/django.yaml @@ -5,7 +5,7 @@ on: jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - name: Start MySQL run: | @@ -13,29 +13,20 @@ jobs: mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -uroot -proot mysql mysql -uroot -proot -e "CREATE USER 'scott'@'%' IDENTIFIED BY 'tiger'; GRANT ALL ON *.* TO scott;" - - uses: actions/cache@v2 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-django-pip-1 - restore-keys: | - ${{ runner.os }}-pip- + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: # https://www.mail-archive.com/django-updates@googlegroups.com/msg209056.html python-version: "3.8" - - uses: actions/checkout@v2 - with: - fetch-depth: 2 - - name: Install mysqlclient env: PIP_NO_PYTHON_VERSION_WARNING: 1 PIP_DISABLE_PIP_VERSION_CHECK: 1 run: | - pip install -U pytest pytest-cov tblib + pip install -r requirements.txt pip install . # pip install mysqlclient # Use stable version diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 0b34ecb4..73681427 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -6,29 +6,20 @@ on: jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - name: Start MySQL run: | sudo systemctl start mysql.service mysql -uroot -proot -e "CREATE DATABASE mysqldb_test" - - uses: actions/cache@v2 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-1 - restore-keys: | - ${{ runner.os }}-pip- - - - uses: actions/checkout@v2 - with: - fetch-depth: 2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -37,7 +28,7 @@ jobs: PIP_NO_PYTHON_VERSION_WARNING: 1 PIP_DISABLE_PIP_VERSION_CHECK: 1 run: | - pip install -U coverage pytest pytest-cov + pip install -r requirements.txt python setup.py develop - name: Run tests @@ -46,4 +37,4 @@ jobs: run: | pytest --cov=MySQLdb tests - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 diff --git a/metadata.cfg b/metadata.cfg index 87ebc6c5..38deff56 100644 --- a/metadata.cfg +++ b/metadata.cfg @@ -21,7 +21,6 @@ classifiers: Programming Language :: C Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..e2546870 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +# This file is for GitHub Action +coverage +pytest +pytest-cov +tblib From 869fe107af08750ea839dfba16e3d58f7d611b61 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 9 May 2023 17:25:18 +0900 Subject: [PATCH 22/39] Update Django test workflow (#594) Django 3.2 LTS will be supported until 2024-04. --- .github/workflows/django.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/django.yaml b/.github/workflows/django.yaml index 55497767..6cf0ed0c 100644 --- a/.github/workflows/django.yaml +++ b/.github/workflows/django.yaml @@ -19,7 +19,7 @@ jobs: uses: actions/setup-python@v4 with: # https://www.mail-archive.com/django-updates@googlegroups.com/msg209056.html - python-version: "3.8" + python-version: "3.11" - name: Install mysqlclient env: @@ -32,7 +32,7 @@ jobs: - name: Run Django test env: - DJANGO_VERSION: "2.2.24" + DJANGO_VERSION: "3.2.19" run: | sudo apt-get install libmemcached-dev wget https://github.com/django/django/archive/${DJANGO_VERSION}.tar.gz From 1f4fb4d98a016c23751c8b9b67ad840e5e79e4df Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 9 May 2023 23:35:52 +0900 Subject: [PATCH 23/39] CI: Update codeql build. (#595) --- .github/workflows/codeql.yml | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 90a8e5b0..ddc6d6e2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -17,26 +17,22 @@ jobs: contents: read security-events: write - strategy: - fail-fast: false - matrix: - language: [ python, cpp ] - steps: - name: Checkout uses: actions/checkout@v3 - name: Initialize CodeQL uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - queries: +security-and-quality + # with: + # languages: ${{ matrix.language }} + # queries: +security-and-quality + + # - name: Autobuild + # uses: github/codeql-action/autobuild@v2 - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - if: ${{ matrix.language == 'python' || matrix.language == 'cpp' }} + - name: Build + run: | + python setup.py build_ext -if - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 - with: - category: "/language:${{ matrix.language }}" From b7255d3aea843b7c039093e77418305b1f956c08 Mon Sep 17 00:00:00 2001 From: piglei Date: Wed, 10 May 2023 14:37:57 +0800 Subject: [PATCH 24/39] Improved exception handling when importing the module (#596) The current expection handling is too vague, and in certain circumstances, the error message may confuse the user. For example, if an error occurs while importing the "_mysql" module, the original error message is as follows: ``` File "MySQLdb/__init__.py", line 18, in from . import _mysql ImportError: /lib64/libstdc++.so.6: cannot allocate memory in static TLS block ``` But on the user side, he can only see the exception message like this: ``` /MySQLdb/__init__.py", line 24, in version_info, _mysql.version_info, _mysql.__file__ NameError: name '_mysql' is not defined ``` This PR fixes this issue by making the exception handling statements more precise. --- MySQLdb/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/MySQLdb/__init__.py b/MySQLdb/__init__.py index b567363b..2851b9bc 100644 --- a/MySQLdb/__init__.py +++ b/MySQLdb/__init__.py @@ -13,12 +13,11 @@ MySQLdb.converters module. """ -try: - from MySQLdb.release import version_info - from . import _mysql +# Check if the version of _mysql matches the version of MySQLdb. +from MySQLdb.release import version_info +from . import _mysql - assert version_info == _mysql.version_info -except Exception: +if version_info != _mysql.version_info: raise ImportError( "this is MySQLdb version {}, but _mysql is version {!r}\n_mysql: {!r}".format( version_info, _mysql.version_info, _mysql.__file__ From cbd894c3b14267d75e22dadb6b186e0508394779 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 10 May 2023 17:15:22 +0900 Subject: [PATCH 25/39] CI: Fix django workflow (#597) --- .github/workflows/django.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/django.yaml b/.github/workflows/django.yaml index 6cf0ed0c..b09d7823 100644 --- a/.github/workflows/django.yaml +++ b/.github/workflows/django.yaml @@ -12,6 +12,7 @@ jobs: sudo systemctl start mysql.service mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -uroot -proot mysql mysql -uroot -proot -e "CREATE USER 'scott'@'%' IDENTIFIED BY 'tiger'; GRANT ALL ON *.* TO scott;" + mysql -uroot -proot -e "CREATE DATABASE django_test; CREATE DATABASE django_other;" - uses: actions/checkout@v3 From 89c1e0f3c6353ac0cf5104f1a5cfe6dafeb28938 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Mon, 15 May 2023 13:22:35 +0900 Subject: [PATCH 26/39] Use pyproject.toml (#598) --- .gitignore | 1 - MANIFEST.in | 5 -- Makefile | 5 ++ MySQLdb/__init__.py | 9 +-- MySQLdb/release.py | 3 + metadata.cfg | 41 ---------- pyproject.toml | 48 ++++++++++++ setup.py | 177 ++++++++++++++++++++++++++++++++++++++++---- setup_common.py | 37 --------- setup_posix.py | 94 ----------------------- setup_windows.py | 64 ---------------- 11 files changed, 223 insertions(+), 261 deletions(-) create mode 100644 MySQLdb/release.py delete mode 100644 metadata.cfg create mode 100644 pyproject.toml delete mode 100644 setup_common.py delete mode 100644 setup_posix.py delete mode 100644 setup_windows.py diff --git a/.gitignore b/.gitignore index 42bbfb5d..1f081cc1 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,4 @@ .tox/ build/ dist/ -MySQLdb/release.py .coverage diff --git a/MANIFEST.in b/MANIFEST.in index 07563caf..58a996de 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,12 +1,7 @@ recursive-include doc *.rst recursive-include tests *.py include doc/conf.py -include MANIFEST.in include HISTORY.rst include README.md include LICENSE -include metadata.cfg include site.cfg -include setup_common.py -include setup_posix.py -include setup_windows.py diff --git a/Makefile b/Makefile index 783d1919..850e296e 100644 --- a/Makefile +++ b/Makefile @@ -14,3 +14,8 @@ clean: find . -name '*.pyc' -delete find . -name '__pycache__' -delete rm -rf build + +.PHONY: check +check: + ruff . + black *.py MySQLdb diff --git a/MySQLdb/__init__.py b/MySQLdb/__init__.py index 2851b9bc..153bbdfe 100644 --- a/MySQLdb/__init__.py +++ b/MySQLdb/__init__.py @@ -13,15 +13,14 @@ MySQLdb.converters module. """ -# Check if the version of _mysql matches the version of MySQLdb. -from MySQLdb.release import version_info +from .release import version_info from . import _mysql if version_info != _mysql.version_info: raise ImportError( - "this is MySQLdb version {}, but _mysql is version {!r}\n_mysql: {!r}".format( - version_info, _mysql.version_info, _mysql.__file__ - ) + f"this is MySQLdb version {version_info}, " + f"but _mysql is version {_mysql.version_info!r}\n" + f"_mysql: {_mysql.__file__!r}" ) diff --git a/MySQLdb/release.py b/MySQLdb/release.py new file mode 100644 index 00000000..55359628 --- /dev/null +++ b/MySQLdb/release.py @@ -0,0 +1,3 @@ +__author__ = "Inada Naoki " +version_info = (2, 2, 0, "dev", 0) +__version__ = "2.2.0.dev0" diff --git a/metadata.cfg b/metadata.cfg deleted file mode 100644 index 38deff56..00000000 --- a/metadata.cfg +++ /dev/null @@ -1,41 +0,0 @@ -[metadata] -name: mysqlclient -version: 2.2.0dev0 -version_info: (2,2,0,'dev',0) -description: Python interface to MySQL -author: Inada Naoki -author_email: songofacandy@gmail.com -license: GPL -platforms: ALL -url: https://github.com/PyMySQL/mysqlclient -classifiers: - Development Status :: 5 - Production/Stable - Environment :: Other Environment - License :: OSI Approved :: GNU General Public License (GPL) - Operating System :: MacOS :: MacOS X - Operating System :: Microsoft :: Windows :: Windows NT/2000 - Operating System :: OS Independent - Operating System :: POSIX - Operating System :: POSIX :: Linux - Operating System :: Unix - Programming Language :: C - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Topic :: Database - Topic :: Database :: Database Engines/Servers -py_modules: - MySQLdb._exceptions - MySQLdb.connections - MySQLdb.converters - MySQLdb.cursors - MySQLdb.release - MySQLdb.times - MySQLdb.constants.CLIENT - MySQLdb.constants.CR - MySQLdb.constants.ER - MySQLdb.constants.FIELD_TYPE - MySQLdb.constants.FLAG diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..907bf55f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,48 @@ +[project] +name = "mysqlclient" +# version = "2.2.0dev0" +description = "Python interface to MySQL" +readme = "README.md" +requires-python = ">=3.8" +authors = [ + {name = "Inada Naoki", email = "songofacandy@gmail.com"} +] +license = {text = "GNU General Public License v2 (GPLv2)"} +keywords = ["MySQL"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Other Environment", + "License :: OSI Approved :: GNU General Public License (GPL)", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows :: Windows NT/2000", + "Operating System :: OS Independent", + "Operating System :: POSIX", + "Operating System :: POSIX :: Linux", + "Operating System :: Unix", + "Programming Language :: C", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Database", + "Topic :: Database :: Database Engines/Servers", +] +dynamic = ["version"] + +[project.urls] +Project = "https://github.com/PyMySQL/mysqlclient" +Documentation = "https://mysqlclient.readthedocs.io/" + +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +namespaces = false +include = ["MySQLdb*"] +exclude = ["tests*", "pymysql.tests*"] + +[tool.setuptools.dynamic] +version = {attr = "MySQLdb.release.__version__"} diff --git a/setup.py b/setup.py index aa6c34fb..368617ef 100644 --- a/setup.py +++ b/setup.py @@ -1,22 +1,171 @@ #!/usr/bin/env python - import os +import subprocess +import sys import setuptools +from configparser import ConfigParser + + +release_info = {} +with open("MySQLdb/release.py", encoding="utf-8") as f: + exec(f.read(), None, release_info) + + +def find_package_name(): + """Get available pkg-config package name""" + packages = ["mysqlclient", "mariadb"] + for pkg in packages: + try: + cmd = f"pkg-config --exists {pkg}" + print(f"Trying {cmd}") + subprocess.check_call(cmd, shell=True) + except subprocess.CalledProcessError as err: + print(err) + else: + return pkg + raise Exception( + "Can not find valid pkg-config name.\n" + "Specify MYSQLCLIENT_CFLAGS and MYSQLCLIENT_LDFLAGS env vars manually" + ) + + +def get_config_posix(options=None): + # allow a command-line option to override the base config file to permit + # a static build to be created via requirements.txt + # TODO: find a better way for + static = False + if "--static" in sys.argv: + static = True + sys.argv.remove("--static") + + ldflags = os.environ.get("MYSQLCLIENT_LDFLAGS") + cflags = os.environ.get("MYSQLCLIENT_CFLAGS") + + pkg_name = None + static_opt = " --static" if static else "" + if not (cflags and ldflags): + pkg_name = find_package_name() + if not cflags: + cflags = subprocess.check_output( + f"pkg-config{static_opt} --cflags {pkg_name}", encoding="utf-8", shell=True + ) + if not ldflags: + ldflags = subprocess.check_output( + f"pkg-config{static_opt} --libs {pkg_name}", encoding="utf-8", shell=True + ) + + cflags = cflags.split() + for f in cflags: + if f.startswith("-std="): + break + else: + cflags += ["-std=c99"] + + ldflags = ldflags.split() + + define_macros = [ + ("version_info", release_info["version_info"]), + ("__version__", release_info["__version__"]), + ] + + ext_options = dict( + extra_compile_args=cflags, + extra_link_args=ldflags, + define_macros=define_macros, + ) + # newer versions of gcc require libstdc++ if doing a static build + if static: + ext_options["language"] = "c++" + + print("Options for building extention module:") + for k, v in ext_options.items(): + print(f" {k}: {v}") + + return ext_options + + +def get_config_win32(options): + 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 = [] + + 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_link_args = ["/MANIFEST"] + + define_macros = [ + ("version_info", release_info["version_info"]), + ("__version__", release_info["__version__"]), + ] + + ext_options = dict( + 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 ext_options + + +def enabled(options, option): + value = options[option] + s = value.lower() + if s in ("yes", "true", "1", "y"): + return True + elif s in ("no", "false", "0", "n"): + return False + else: + raise ValueError(f"Unknown value {value} for option {option}") + + +def get_options(): + config = ConfigParser() + config.read(["site.cfg"]) + options = dict(config.items("options")) + options["static"] = enabled(options, "static") + return options + -if os.name == "posix": - from setup_posix import get_config -else: # assume windows - from setup_windows import get_config +if sys.platform == "win32": + ext_options = get_config_win32(get_options()) +else: + ext_options = get_config_posix(get_options()) -with open("README.md", encoding="utf-8") as f: - readme = f.read() +print("# Extention options") +for k, v in ext_options.items(): + print(f" {k}: {v}") -metadata, options = get_config() -metadata["ext_modules"] = [ - setuptools.Extension("MySQLdb._mysql", sources=["MySQLdb/_mysql.c"], **options) +ext_modules = [ + setuptools.Extension( + "MySQLdb._mysql", + sources=["MySQLdb/_mysql.c"], + **ext_options, + ) ] -metadata["long_description"] = readme -metadata["long_description_content_type"] = "text/markdown" -metadata["python_requires"] = ">=3.7" -setuptools.setup(**metadata) +setuptools.setup(ext_modules=ext_modules) diff --git a/setup_common.py b/setup_common.py deleted file mode 100644 index 53869aa2..00000000 --- a/setup_common.py +++ /dev/null @@ -1,37 +0,0 @@ -from configparser import ConfigParser - - -def get_metadata_and_options(): - config = ConfigParser() - config.read(["metadata.cfg", "site.cfg"]) - - 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"))) - - return metadata, options - - -def enabled(options, option): - value = options[option] - s = value.lower() - if s in ("yes", "true", "1", "y"): - return True - elif s in ("no", "false", "0", "n"): - return False - else: - raise ValueError(f"Unknown value {value} for option {option}") - - -def create_release_file(metadata): - 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 - ) diff --git a/setup_posix.py b/setup_posix.py deleted file mode 100644 index a03dd22c..00000000 --- a/setup_posix.py +++ /dev/null @@ -1,94 +0,0 @@ -import os -import sys -import subprocess - - -def find_package_name(): - """Get available pkg-config package name""" - packages = ["mysqlclient", "mariadb"] - for pkg in packages: - try: - cmd = f"pkg-config --exists {pkg}" - print(f"Trying {cmd}") - subprocess.check_call(cmd, shell=True) - except subprocess.CalledProcessError as err: - print(err) - else: - return pkg - raise Exception("Can not find valid pkg-config") - - -def get_config(): - from setup_common import get_metadata_and_options, enabled, create_release_file - - metadata, options = get_metadata_and_options() - - 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: - static = True - sys.argv.remove("--static") - - ldflags = os.environ.get("MYSQLCLIENT_LDFLAGS") - cflags = os.environ.get("MYSQLCLIENT_CFLAGS") - - pkg_name = None - static_opt = " --static" if static else "" - if not (cflags and ldflags): - pkg_name = find_package_name() - if not cflags: - cflags = subprocess.check_output( - f"pkg-config{static_opt} --cflags {pkg_name}", encoding="utf-8", shell=True - ) - if not ldflags: - ldflags = subprocess.check_output( - f"pkg-config{static_opt} --libs {pkg_name}", encoding="utf-8", shell=True - ) - - cflags = cflags.split() - for f in cflags: - if f.startswith("-std="): - break - else: - cflags += ["-std=c99"] - - ldflags = ldflags.split() - - define_macros = [ - ("version_info", metadata["version_info"]), - ("__version__", metadata["version"]), - ] - - # print(f"{cflags = }") - # print(f"{ldflags = }") - # print(f"{define_macros = }") - - ext_options = dict( - extra_compile_args=cflags, - extra_link_args=ldflags, - define_macros=define_macros, - ) - # newer versions of gcc require libstdc++ if doing a static build - if static: - ext_options["language"] = "c++" - - print("Options for building extention module:") - for k, v in ext_options.items(): - print(f" {k}: {v}") - - create_release_file(metadata) - del metadata["version_info"] - - return metadata, ext_options - - -if __name__ == "__main__": - from pprint import pprint - - metadata, config = get_config() - print("# Metadata") - pprint(metadata, sort_dicts=False, compact=True) - print("\n# Extention options") - pprint(config, sort_dicts=False, compact=True) diff --git a/setup_windows.py b/setup_windows.py deleted file mode 100644 index 5d8d7158..00000000 --- a/setup_windows.py +++ /dev/null @@ -1,64 +0,0 @@ -import os - - -def get_config(): - from setup_common import get_metadata_and_options, create_release_file - - metadata, options = get_metadata_and_options() - - 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 = [] - - 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_link_args = ["/MANIFEST"] - - define_macros = [ - ("version_info", metadata["version_info"]), - ("__version__", metadata["version"]), - ] - create_release_file(metadata) - del metadata["version_info"] - ext_options = dict( - 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__": - from pprint import pprint - - metadata, config = get_config() - print("# Metadata") - pprint(metadata) - print("\n# Extention options") - pprint(config) From 9953e509c4f712d45a4f5060ddbd65c3026bea63 Mon Sep 17 00:00:00 2001 From: Steve Teahan Date: Mon, 15 May 2023 01:17:21 -0400 Subject: [PATCH 27/39] Add Cursor.mogrify(). (#477) Implements #476 Co-authored-by: Inada Naoki --- MySQLdb/cursors.py | 27 ++++++++++++++++++++++++--- tests/test_cursor.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/MySQLdb/cursors.py b/MySQLdb/cursors.py index f8a48640..fdf52c0b 100644 --- a/MySQLdb/cursors.py +++ b/MySQLdb/cursors.py @@ -182,6 +182,15 @@ def execute(self, query, args=None): """ while self.nextset(): pass + + mogrified_query = self._mogrify(query, args) + + assert isinstance(mogrified_query, (bytes, bytearray)) + res = self._query(mogrified_query) + return res + + def _mogrify(self, query, args=None): + """Return query after binding args.""" db = self._get_db() if isinstance(query, str): @@ -202,9 +211,21 @@ def execute(self, query, args=None): except TypeError as m: raise ProgrammingError(str(m)) - assert isinstance(query, (bytes, bytearray)) - res = self._query(query) - return res + return query + + def mogrify(self, query, args=None): + """Return query after binding args. + + query -- string, query to mogrify + args -- optional sequence or mapping, parameters to use with query. + + Note: If args is a sequence, then %s must be used as the + parameter placeholder in the query. If a mapping is used, + %(key)s must be used as the placeholder. + + Returns string representing query that would be executed by the server + """ + return self._mogrify(query, args).decode(self._get_db().encoding) def executemany(self, query, args): # type: (str, list) -> int diff --git a/tests/test_cursor.py b/tests/test_cursor.py index 80e21888..c681b63b 100644 --- a/tests/test_cursor.py +++ b/tests/test_cursor.py @@ -150,3 +150,39 @@ def test_dictcursor(): names2 = sorted(rows[1]) for a, b in zip(names1, names2): assert a is b + + +def test_mogrify_without_args(): + conn = connect() + cursor = conn.cursor() + + query = "SELECT VERSION()" + mogrified_query = cursor.mogrify(query) + cursor.execute(query) + + assert mogrified_query == query + assert mogrified_query == cursor._executed.decode() + + +def test_mogrify_with_tuple_args(): + conn = connect() + cursor = conn.cursor() + + query_with_args = "SELECT %s, %s", (1, 2) + mogrified_query = cursor.mogrify(*query_with_args) + cursor.execute(*query_with_args) + + assert mogrified_query == "SELECT 1, 2" + assert mogrified_query == cursor._executed.decode() + + +def test_mogrify_with_dict_args(): + conn = connect() + cursor = conn.cursor() + + query_with_args = "SELECT %(a)s, %(b)s", {"a": 1, "b": 2} + mogrified_query = cursor.mogrify(*query_with_args) + cursor.execute(*query_with_args) + + assert mogrified_query == "SELECT 1, 2" + assert mogrified_query == cursor._executed.decode() From abb139bc5471ad59be214d20a76323c0f97a2193 Mon Sep 17 00:00:00 2001 From: Teodor Moroz Date: Mon, 15 May 2023 12:09:04 +0300 Subject: [PATCH 28/39] Support ssl_mode setting with mariadb client (#475) According to https://mariadb.com/kb/en/mysql_optionsv/ MariaDB supports TLS enforcing in its own way. So the idea behind this PR is to keep the same interface for MariaDB based clients, but behind the scenes handle it accordingly.(MariaDB gets its own args set, instead of ssl_mode dict supported by MySQL). Co-authored-by: Teodor Moroz Co-authored-by: Inada Naoki --- MySQLdb/_mysql.c | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/MySQLdb/_mysql.c b/MySQLdb/_mysql.c index 6c04ec99..4463f627 100644 --- a/MySQLdb/_mysql.c +++ b/MySQLdb/_mysql.c @@ -380,7 +380,14 @@ static int _mysql_ResultObject_clear(_mysql_ResultObject *self) return 0; } -#ifdef HAVE_ENUM_MYSQL_OPT_SSL_MODE +enum { + SSLMODE_DISABLED = 1, + SSLMODE_PREFERRED = 2, + SSLMODE_REQUIRED = 3, + SSLMODE_VERIFY_CA = 4, + SSLMODE_VERIFY_IDENTITY = 5 +}; + static int _get_ssl_mode_num(char *ssl_mode) { @@ -395,7 +402,6 @@ _get_ssl_mode_num(char *ssl_mode) } return -1; } -#endif static int _mysql_ConnectionObject_Initialize( @@ -429,6 +435,7 @@ _mysql_ConnectionObject_Initialize( int read_timeout = 0; int write_timeout = 0; int compress = -1, named_pipe = -1, local_infile = -1; + int ssl_mode_num = SSLMODE_DISABLED; char *init_command=NULL, *read_default_file=NULL, *read_default_group=NULL, @@ -469,15 +476,10 @@ _mysql_ConnectionObject_Initialize( _stringsuck(cipher, value, ssl); } if (ssl_mode) { -#ifdef HAVE_ENUM_MYSQL_OPT_SSL_MODE - if (_get_ssl_mode_num(ssl_mode) <= 0) { + if ((ssl_mode_num = _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)); @@ -487,6 +489,7 @@ _mysql_ConnectionObject_Initialize( } Py_BEGIN_ALLOW_THREADS ; self->open = 1; + if (connect_timeout) { unsigned int timeout = connect_timeout; mysql_options(&(self->connection), MYSQL_OPT_CONNECT_TIMEOUT, @@ -521,12 +524,23 @@ _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); +#ifdef HAVE_ENUM_MYSQL_OPT_SSL_MODE mysql_options(&(self->connection), MYSQL_OPT_SSL_MODE, &ssl_mode_num); - } +#else + // MariaDB doesn't support MYSQL_OPT_SSL_MODE. + // See https://github.com/PyMySQL/mysqlclient/issues/474 + // TODO: Does MariaDB supports PREFERRED and VERIFY_CA? + // We support only two levels for now. + if (sslmode_num >= SSLMODE_REQUIRED) { + mysql_optionsv(&(self->connection), MYSQL_OPT_SSL_ENFORCE, (void *)&enforce_tls); + } + if (sslmode_num >= SSLMODE_VERIFY_CA) { + mysql_optionsv(&(self->connection), MYSQL_OPT_SSL_VERIFY_SERVER_CERT, (void *)&enforce_tls); + } #endif + } + if (charset) { mysql_options(&(self->connection), MYSQL_SET_CHARSET_NAME, charset); } From 0220f427a9102c0c293ba2d91f8cad866109e406 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Mon, 15 May 2023 23:50:10 +0900 Subject: [PATCH 29/39] Use src-layout. (#600) --- .github/workflows/tests.yaml | 2 +- Makefile | 6 +++--- codecov.yml | 2 +- pyproject.toml | 5 ++++- setup.py | 4 ++-- {MySQLdb => src/MySQLdb}/__init__.py | 0 {MySQLdb => src/MySQLdb}/_exceptions.py | 0 {MySQLdb => src/MySQLdb}/_mysql.c | 0 {MySQLdb => src/MySQLdb}/connections.py | 0 {MySQLdb => src/MySQLdb}/constants/CLIENT.py | 0 {MySQLdb => src/MySQLdb}/constants/CR.py | 0 {MySQLdb => src/MySQLdb}/constants/ER.py | 0 {MySQLdb => src/MySQLdb}/constants/FIELD_TYPE.py | 0 {MySQLdb => src/MySQLdb}/constants/FLAG.py | 0 {MySQLdb => src/MySQLdb}/constants/__init__.py | 0 {MySQLdb => src/MySQLdb}/converters.py | 0 {MySQLdb => src/MySQLdb}/cursors.py | 0 {MySQLdb => src/MySQLdb}/release.py | 0 {MySQLdb => src/MySQLdb}/times.py | 0 19 files changed, 11 insertions(+), 8 deletions(-) rename {MySQLdb => src/MySQLdb}/__init__.py (100%) rename {MySQLdb => src/MySQLdb}/_exceptions.py (100%) rename {MySQLdb => src/MySQLdb}/_mysql.c (100%) rename {MySQLdb => src/MySQLdb}/connections.py (100%) rename {MySQLdb => src/MySQLdb}/constants/CLIENT.py (100%) rename {MySQLdb => src/MySQLdb}/constants/CR.py (100%) rename {MySQLdb => src/MySQLdb}/constants/ER.py (100%) rename {MySQLdb => src/MySQLdb}/constants/FIELD_TYPE.py (100%) rename {MySQLdb => src/MySQLdb}/constants/FLAG.py (100%) rename {MySQLdb => src/MySQLdb}/constants/__init__.py (100%) rename {MySQLdb => src/MySQLdb}/converters.py (100%) rename {MySQLdb => src/MySQLdb}/cursors.py (100%) rename {MySQLdb => src/MySQLdb}/release.py (100%) rename {MySQLdb => src/MySQLdb}/times.py (100%) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 73681427..31ee34d3 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -29,7 +29,7 @@ jobs: PIP_DISABLE_PIP_VERSION_CHECK: 1 run: | pip install -r requirements.txt - python setup.py develop + pip install . - name: Run tests env: diff --git a/Makefile b/Makefile index 850e296e..f0e94c3b 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: build build: - python3 setup.py build_ext -if + python setup.py build_ext -if .PHONY: doc doc: @@ -10,7 +10,7 @@ doc: .PHONY: clean clean: - python3 setup.py clean + python setup.py clean find . -name '*.pyc' -delete find . -name '__pycache__' -delete rm -rf build @@ -18,4 +18,4 @@ clean: .PHONY: check check: ruff . - black *.py MySQLdb + black *.py src diff --git a/codecov.yml b/codecov.yml index 174a4994..014486d2 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,2 +1,2 @@ ignore: - - "MySQLdb/constants/*" + - "src/MySQLdb/constants/*" diff --git a/pyproject.toml b/pyproject.toml index 907bf55f..3bfd1f6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,10 +39,13 @@ Documentation = "https://mysqlclient.readthedocs.io/" requires = ["setuptools>=61"] build-backend = "setuptools.build_meta" +[tool.setuptools] +package-dir = {"" = "src"} + [tool.setuptools.packages.find] namespaces = false +where = ["src"] include = ["MySQLdb*"] -exclude = ["tests*", "pymysql.tests*"] [tool.setuptools.dynamic] version = {attr = "MySQLdb.release.__version__"} diff --git a/setup.py b/setup.py index 368617ef..5594df54 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ release_info = {} -with open("MySQLdb/release.py", encoding="utf-8") as f: +with open("src/MySQLdb/release.py", encoding="utf-8") as f: exec(f.read(), None, release_info) @@ -164,7 +164,7 @@ def get_options(): ext_modules = [ setuptools.Extension( "MySQLdb._mysql", - sources=["MySQLdb/_mysql.c"], + sources=["src/MySQLdb/_mysql.c"], **ext_options, ) ] diff --git a/MySQLdb/__init__.py b/src/MySQLdb/__init__.py similarity index 100% rename from MySQLdb/__init__.py rename to src/MySQLdb/__init__.py diff --git a/MySQLdb/_exceptions.py b/src/MySQLdb/_exceptions.py similarity index 100% rename from MySQLdb/_exceptions.py rename to src/MySQLdb/_exceptions.py diff --git a/MySQLdb/_mysql.c b/src/MySQLdb/_mysql.c similarity index 100% rename from MySQLdb/_mysql.c rename to src/MySQLdb/_mysql.c diff --git a/MySQLdb/connections.py b/src/MySQLdb/connections.py similarity index 100% rename from MySQLdb/connections.py rename to src/MySQLdb/connections.py diff --git a/MySQLdb/constants/CLIENT.py b/src/MySQLdb/constants/CLIENT.py similarity index 100% rename from MySQLdb/constants/CLIENT.py rename to src/MySQLdb/constants/CLIENT.py diff --git a/MySQLdb/constants/CR.py b/src/MySQLdb/constants/CR.py similarity index 100% rename from MySQLdb/constants/CR.py rename to src/MySQLdb/constants/CR.py diff --git a/MySQLdb/constants/ER.py b/src/MySQLdb/constants/ER.py similarity index 100% rename from MySQLdb/constants/ER.py rename to src/MySQLdb/constants/ER.py diff --git a/MySQLdb/constants/FIELD_TYPE.py b/src/MySQLdb/constants/FIELD_TYPE.py similarity index 100% rename from MySQLdb/constants/FIELD_TYPE.py rename to src/MySQLdb/constants/FIELD_TYPE.py diff --git a/MySQLdb/constants/FLAG.py b/src/MySQLdb/constants/FLAG.py similarity index 100% rename from MySQLdb/constants/FLAG.py rename to src/MySQLdb/constants/FLAG.py diff --git a/MySQLdb/constants/__init__.py b/src/MySQLdb/constants/__init__.py similarity index 100% rename from MySQLdb/constants/__init__.py rename to src/MySQLdb/constants/__init__.py diff --git a/MySQLdb/converters.py b/src/MySQLdb/converters.py similarity index 100% rename from MySQLdb/converters.py rename to src/MySQLdb/converters.py diff --git a/MySQLdb/cursors.py b/src/MySQLdb/cursors.py similarity index 100% rename from MySQLdb/cursors.py rename to src/MySQLdb/cursors.py diff --git a/MySQLdb/release.py b/src/MySQLdb/release.py similarity index 100% rename from MySQLdb/release.py rename to src/MySQLdb/release.py diff --git a/MySQLdb/times.py b/src/MySQLdb/times.py similarity index 100% rename from MySQLdb/times.py rename to src/MySQLdb/times.py From a2e970698fcc5af401092016c474260c2f267655 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 17 May 2023 02:08:52 +0900 Subject: [PATCH 30/39] Discard results without converting them into Python objects. (#601) Fixes #560. --- src/MySQLdb/_mysql.c | 69 ++++++++++++++++++++++++++++++++++++++++++ src/MySQLdb/cursors.py | 24 ++++++++++++--- tests/test_cursor.py | 38 ++++++++++++++++++++++- 3 files changed, 126 insertions(+), 5 deletions(-) diff --git a/src/MySQLdb/_mysql.c b/src/MySQLdb/_mysql.c index 4463f627..b8a19d26 100644 --- a/src/MySQLdb/_mysql.c +++ b/src/MySQLdb/_mysql.c @@ -1484,6 +1484,26 @@ _mysql_ResultObject_fetch_row( return NULL; } +static const char _mysql_ResultObject_discard__doc__[] = +"discard() -- Discard remaining rows in the resultset."; + +static PyObject * +_mysql_ResultObject_discard( + _mysql_ResultObject *self, + PyObject *noargs) +{ + check_result_connection(self); + + MYSQL_ROW row; + while (NULL != (row = mysql_fetch_row(self->result))) { + // do nothing + } + if (mysql_errno(self->conn)) { + return _mysql_Exception(self->conn); + } + Py_RETURN_NONE; +} + static char _mysql_ConnectionObject_change_user__doc__[] = "Changes the user and causes the database specified by db to\n\ become the default (current) database on the connection\n\ @@ -2081,6 +2101,43 @@ _mysql_ConnectionObject_use_result( return result; } +static const char _mysql_ConnectionObject_discard_result__doc__[] = +"Discard current result set.\n\n" +"This function can be called instead of use_result() or store_result(). Non-standard."; + +static PyObject * +_mysql_ConnectionObject_discard_result( + _mysql_ConnectionObject *self, + PyObject *noargs) +{ + check_connection(self); + MYSQL *conn = &(self->connection); + + Py_BEGIN_ALLOW_THREADS; + + MYSQL_RES *res = mysql_use_result(conn); + if (res == NULL) { + Py_BLOCK_THREADS; + if (mysql_errno(conn) != 0) { + // fprintf(stderr, "mysql_use_result failed: %s\n", mysql_error(conn)); + return _mysql_Exception(self); + } + Py_RETURN_NONE; + } + + MYSQL_ROW row; + while (NULL != (row = mysql_fetch_row(res))) { + // do nothing. + } + mysql_free_result(res); + Py_END_ALLOW_THREADS; + if (mysql_errno(conn)) { + // fprintf(stderr, "mysql_free_result failed: %s\n", mysql_error(conn)); + return _mysql_Exception(self); + } + Py_RETURN_NONE; +} + static void _mysql_ConnectionObject_dealloc( _mysql_ConnectionObject *self) @@ -2376,6 +2433,12 @@ static PyMethodDef _mysql_ConnectionObject_methods[] = { METH_NOARGS, _mysql_ConnectionObject_use_result__doc__ }, + { + "discard_result", + (PyCFunction)_mysql_ConnectionObject_discard_result, + METH_NOARGS, + _mysql_ConnectionObject_discard_result__doc__ + }, {NULL, NULL} /* sentinel */ }; @@ -2437,6 +2500,12 @@ static PyMethodDef _mysql_ResultObject_methods[] = { METH_VARARGS | METH_KEYWORDS, _mysql_ResultObject_fetch_row__doc__ }, + { + "discard", + (PyCFunction)_mysql_ResultObject_discard, + METH_NOARGS, + _mysql_ResultObject_discard__doc__ + }, { "field_flags", (PyCFunction)_mysql_ResultObject_field_flags, diff --git a/src/MySQLdb/cursors.py b/src/MySQLdb/cursors.py index fdf52c0b..d3b2947b 100644 --- a/src/MySQLdb/cursors.py +++ b/src/MySQLdb/cursors.py @@ -75,13 +75,30 @@ def __init__(self, connection): self.rownumber = None self._rows = None + def _discard(self): + self.description = None + self.description_flags = None + self.rowcount = -1 + self.lastrowid = None + self._rows = None + self.rownumber = None + + if self._result: + self._result.discard() + self._result = None + + con = self.connection + if con is None: + return + while con.next_result() == 0: # -1 means no more data. + con.discard_result() + def close(self): """Close the cursor. No further queries will be possible.""" try: if self.connection is None: return - while self.nextset(): - pass + self._discard() finally: self.connection = None self._result = None @@ -180,8 +197,7 @@ def execute(self, query, args=None): Returns integer represents rows affected, if any """ - while self.nextset(): - pass + self._discard() mogrified_query = self._mogrify(query, args) diff --git a/tests/test_cursor.py b/tests/test_cursor.py index c681b63b..5cb98910 100644 --- a/tests/test_cursor.py +++ b/tests/test_cursor.py @@ -1,4 +1,4 @@ -# import pytest +import pytest import MySQLdb.cursors from configdb import connection_factory @@ -186,3 +186,39 @@ def test_mogrify_with_dict_args(): assert mogrified_query == "SELECT 1, 2" assert mogrified_query == cursor._executed.decode() + + +# Test that cursor can be used without reading whole resultset. +@pytest.mark.parametrize("Cursor", [MySQLdb.cursors.Cursor, MySQLdb.cursors.SSCursor]) +def test_cursor_discard_result(Cursor): + conn = connect() + cursor = conn.cursor(Cursor) + + cursor.execute( + """\ +CREATE TABLE test_cursor_discard_result ( + id INTEGER PRIMARY KEY AUTO_INCREMENT, + data VARCHAR(100) +)""" + ) + _tables.append("test_cursor_discard_result") + + cursor.executemany( + "INSERT INTO test_cursor_discard_result (id, data) VALUES (%s, %s)", + [(i, f"row {i}") for i in range(1, 101)], + ) + + cursor.execute( + """\ +SELECT * FROM test_cursor_discard_result WHERE id <= 10; +SELECT * FROM test_cursor_discard_result WHERE id BETWEEN 11 AND 20; +SELECT * FROM test_cursor_discard_result WHERE id BETWEEN 21 AND 30; +""" + ) + cursor.nextset() + assert cursor.fetchone() == (11, "row 11") + + cursor.execute( + "SELECT * FROM test_cursor_discard_result WHERE id BETWEEN 31 AND 40" + ) + assert cursor.fetchone() == (31, "row 31") From 3517eb77b73613db308c39c8b3af0e5aa51b8a2a Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 17 May 2023 10:58:18 +0900 Subject: [PATCH 31/39] Fix sphinx warnings (#602) Fix #539 --- doc/MySQLdb.rst | 31 +++++++++++++------------------ doc/conf.py | 7 +++++++ src/MySQLdb/cursors.py | 2 +- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/doc/MySQLdb.rst b/doc/MySQLdb.rst index 134a40b6..5e6791d5 100644 --- a/doc/MySQLdb.rst +++ b/doc/MySQLdb.rst @@ -9,53 +9,48 @@ MySQLdb Package :undoc-members: :show-inheritance: -:mod:`connections` Module -------------------------- +:mod:`MySQLdb.connections` Module +--------------------------------- .. automodule:: MySQLdb.connections :members: Connection :undoc-members: - :show-inheritance: -:mod:`converters` Module ------------------------- +:mod:`MySQLdb.converters` Module +-------------------------------- .. automodule:: MySQLdb.converters :members: :undoc-members: - :show-inheritance: -:mod:`cursors` Module ---------------------- +:mod:`MySQLdb.cursors` Module +----------------------------- .. automodule:: MySQLdb.cursors - :members: Cursor + :members: :undoc-members: :show-inheritance: -:mod:`times` Module -------------------- +:mod:`MySQLdb.times` Module +--------------------------- .. automodule:: MySQLdb.times :members: :undoc-members: - :show-inheritance: -:mod:`_mysql` Module --------------------- +:mod:`MySQLdb._mysql` Module +---------------------------- .. automodule:: MySQLdb._mysql :members: :undoc-members: - :show-inheritance: -:mod:`_exceptions` Module -------------------------- +:mod:`MySQLdb._exceptions` Module +--------------------------------- .. automodule:: MySQLdb._exceptions :members: :undoc-members: - :show-inheritance: Subpackages diff --git a/doc/conf.py b/doc/conf.py index 5d8cd1a0..3e919822 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -22,6 +22,13 @@ # -- General configuration ----------------------------------------------------- +nitpick_ignore = [ + ("py:class", "datetime.date"), + ("py:class", "datetime.time"), + ("py:class", "datetime.datetime"), +] + + # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = "1.0" diff --git a/src/MySQLdb/cursors.py b/src/MySQLdb/cursors.py index d3b2947b..7851359f 100644 --- a/src/MySQLdb/cursors.py +++ b/src/MySQLdb/cursors.py @@ -8,7 +8,7 @@ from ._exceptions import ProgrammingError -#: Regular expression for :meth:`Cursor.executemany`. +#: Regular expression for ``Cursor.executemany```. #: executemany only supports simple bulk insert. #: You can use it to load large dataset. RE_INSERT_VALUES = re.compile( From 3d6b8c9b7c69c8d7f23def2992835d8b93d67a53 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Thu, 18 May 2023 17:19:10 +0900 Subject: [PATCH 32/39] Release GIL during result.discard() (#604) --- src/MySQLdb/_mysql.c | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/MySQLdb/_mysql.c b/src/MySQLdb/_mysql.c index b8a19d26..b030af16 100644 --- a/src/MySQLdb/_mysql.c +++ b/src/MySQLdb/_mysql.c @@ -487,7 +487,6 @@ _mysql_ConnectionObject_Initialize( PyErr_SetNone(PyExc_MemoryError); return -1; } - Py_BEGIN_ALLOW_THREADS ; self->open = 1; if (connect_timeout) { @@ -548,10 +547,10 @@ _mysql_ConnectionObject_Initialize( mysql_options(&(self->connection), MYSQL_DEFAULT_AUTH, auth_plugin); } + Py_BEGIN_ALLOW_THREADS conn = mysql_real_connect(&(self->connection), host, user, passwd, db, port, unix_socket, client_flag); - - Py_END_ALLOW_THREADS ; + Py_END_ALLOW_THREADS if (ssl) { int i; @@ -1403,9 +1402,9 @@ _mysql__fetch_row( if (!self->use) row = mysql_fetch_row(self->result); else { - Py_BEGIN_ALLOW_THREADS; + Py_BEGIN_ALLOW_THREADS row = mysql_fetch_row(self->result); - Py_END_ALLOW_THREADS; + Py_END_ALLOW_THREADS } if (!row && mysql_errno(&(((_mysql_ConnectionObject *)(self->conn))->connection))) { _mysql_Exception((_mysql_ConnectionObject *)self->conn); @@ -1495,9 +1494,11 @@ _mysql_ResultObject_discard( check_result_connection(self); MYSQL_ROW row; + Py_BEGIN_ALLOW_THREADS while (NULL != (row = mysql_fetch_row(self->result))) { // do nothing } + Py_END_ALLOW_THREADS if (mysql_errno(self->conn)) { return _mysql_Exception(self->conn); } @@ -1747,9 +1748,7 @@ _mysql_ConnectionObject_insert_id( { my_ulonglong r; check_connection(self); - Py_BEGIN_ALLOW_THREADS r = mysql_insert_id(&(self->connection)); - Py_END_ALLOW_THREADS return PyLong_FromUnsignedLongLong(r); } @@ -2058,9 +2057,7 @@ _mysql_ConnectionObject_thread_id( { unsigned long pid; check_connection(self); - Py_BEGIN_ALLOW_THREADS pid = mysql_thread_id(&(self->connection)); - Py_END_ALLOW_THREADS return PyLong_FromLong((long)pid); } From 62f0645376ca71c39c90ea6e0d360a663e4f13a1 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Thu, 18 May 2023 17:52:38 +0900 Subject: [PATCH 33/39] Fix executemany with binary prefix (#605) Fix #494 --- src/MySQLdb/cursors.py | 34 ++-------------------------------- tests/default.cnf | 5 +++-- tests/test_cursor.py | 23 ++++++++++++++++++++++- 3 files changed, 27 insertions(+), 35 deletions(-) diff --git a/src/MySQLdb/cursors.py b/src/MySQLdb/cursors.py index 7851359f..785fa9a1 100644 --- a/src/MySQLdb/cursors.py +++ b/src/MySQLdb/cursors.py @@ -110,34 +110,6 @@ def __exit__(self, *exc_info): del exc_info self.close() - def _escape_args(self, args, conn): - encoding = conn.encoding - literal = conn.literal - - def ensure_bytes(x): - if isinstance(x, str): - return x.encode(encoding) - elif isinstance(x, tuple): - return tuple(map(ensure_bytes, x)) - elif isinstance(x, list): - return list(map(ensure_bytes, x)) - return x - - 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() - } - else: - # If it's not a dictionary let's try escaping it anyways. - # Worst case it will throw a Value error - ret = literal(ensure_bytes(args)) - - ensure_bytes = None # break circular reference - return ret - def _check_executed(self): if not self._executed: raise ProgrammingError("execute() first") @@ -279,8 +251,6 @@ def executemany(self, query, args): def _do_execute_many( self, prefix, values, postfix, args, max_stmt_length, encoding ): - conn = self._get_db() - escape = self._escape_args if isinstance(prefix, str): prefix = prefix.encode(encoding) if isinstance(values, str): @@ -289,11 +259,11 @@ def _do_execute_many( postfix = postfix.encode(encoding) sql = bytearray(prefix) args = iter(args) - v = values % escape(next(args), conn) + v = self._mogrify(values, next(args)) sql += v rows = 0 for arg in args: - v = values % escape(arg, conn) + v = self._mogrify(values, arg) if len(sql) + len(v) + len(postfix) + 1 > max_stmt_length: rows += self.execute(sql + postfix) sql = bytearray(prefix) diff --git a/tests/default.cnf b/tests/default.cnf index 2aeda7cf..1d6c9421 100644 --- a/tests/default.cnf +++ b/tests/default.cnf @@ -2,9 +2,10 @@ # http://dev.mysql.com/doc/refman/5.1/en/option-files.html # and set TESTDB in your environment to the name of the file +# $ docker run -e MYSQL_ALLOW_EMPTY_PASSWORD=yes -p 3306:3306 --rm --name mysqld mysql:latest [MySQLdb-tests] host = 127.0.0.1 -user = test +user = root database = test #password = -default-character-set = utf8 +default-character-set = utf8mb4 diff --git a/tests/test_cursor.py b/tests/test_cursor.py index 5cb98910..1d2c3655 100644 --- a/tests/test_cursor.py +++ b/tests/test_cursor.py @@ -72,7 +72,7 @@ def test_executemany(): # values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9) # """ # list args - data = range(10) + data = [(i,) for i in range(10)] cursor.executemany("insert into test (data) values (%s)", data) assert cursor._executed.endswith( b",(7),(8),(9)" @@ -222,3 +222,24 @@ def test_cursor_discard_result(Cursor): "SELECT * FROM test_cursor_discard_result WHERE id BETWEEN 31 AND 40" ) assert cursor.fetchone() == (31, "row 31") + + +def test_binary_prefix(): + # https://github.com/PyMySQL/mysqlclient/issues/494 + conn = connect(binary_prefix=True) + cursor = conn.cursor() + + cursor.execute("DROP TABLE IF EXISTS test_binary_prefix") + cursor.execute( + """\ +CREATE TABLE test_binary_prefix ( + id INTEGER NOT NULL AUTO_INCREMENT, + json JSON NOT NULL, + PRIMARY KEY (id) +) CHARSET=utf8mb4""" + ) + + cursor.executemany( + "INSERT INTO test_binary_prefix (id, json) VALUES (%(id)s, %(json)s)", + ({"id": 1, "json": "{}"}, {"id": 2, "json": "{}"}), + ) From 44d0f7a148474a4baefe4bb4a8836e192b6a88db Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Thu, 18 May 2023 18:02:28 +0900 Subject: [PATCH 34/39] CI: Fix Django test (#606) --- .github/workflows/django.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/django.yaml b/.github/workflows/django.yaml index b09d7823..737f34be 100644 --- a/.github/workflows/django.yaml +++ b/.github/workflows/django.yaml @@ -12,7 +12,7 @@ jobs: sudo systemctl start mysql.service mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -uroot -proot mysql mysql -uroot -proot -e "CREATE USER 'scott'@'%' IDENTIFIED BY 'tiger'; GRANT ALL ON *.* TO scott;" - mysql -uroot -proot -e "CREATE DATABASE django_test; CREATE DATABASE django_other;" + mysql -uroot -proot -e "CREATE DATABASE django_default; CREATE DATABASE django_other;" - uses: actions/checkout@v3 From b162dddcf318ddc0362a6c008d4ad165797ab9bb Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Thu, 18 May 2023 20:08:04 +0900 Subject: [PATCH 35/39] Fix Connection.escape() with Unicode input (#608) After aed1dd2, Connection.escape() used ASCII to escape Unicode input. This commit makes it uses connection encoding instead. --- src/MySQLdb/_mysql.c | 64 +++++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/src/MySQLdb/_mysql.c b/src/MySQLdb/_mysql.c index b030af16..1f52d90b 100644 --- a/src/MySQLdb/_mysql.c +++ b/src/MySQLdb/_mysql.c @@ -943,7 +943,7 @@ _mysql_escape_string( { PyObject *str; char *in, *out; - int len; + unsigned long len; Py_ssize_t size; if (!PyArg_ParseTuple(args, "s#:escape_string", &in, &size)) return NULL; str = PyBytes_FromStringAndSize((char *) NULL, size*2+1); @@ -980,10 +980,7 @@ _mysql_string_literal( _mysql_ConnectionObject *self, PyObject *o) { - PyObject *str, *s; - char *in, *out; - unsigned long len; - Py_ssize_t size; + PyObject *s; // input string or bytes. need to decref. if (self && PyModule_Check((PyObject*)self)) self = NULL; @@ -991,24 +988,44 @@ _mysql_string_literal( if (PyBytes_Check(o)) { s = o; Py_INCREF(s); - } else { - s = PyObject_Str(o); - if (!s) return NULL; - { - PyObject *t = PyUnicode_AsASCIIString(s); - Py_DECREF(s); - if (!t) return NULL; + } + else { + PyObject *t = PyObject_Str(o); + if (!t) return NULL; + + const char *encoding = (self && self->open) ? + _get_encoding(&self->connection) : utf8; + if (encoding == utf8) { s = t; } + else { + s = PyUnicode_AsEncodedString(t, encoding, "strict"); + Py_DECREF(t); + if (!s) return NULL; + } } - in = PyBytes_AsString(s); - size = PyBytes_GET_SIZE(s); - str = PyBytes_FromStringAndSize((char *) NULL, size*2+3); + + // Prepare input string (in, size) + const char *in; + Py_ssize_t size; + if (PyUnicode_Check(s)) { + in = PyUnicode_AsUTF8AndSize(s, &size); + } else { + assert(PyBytes_Check(s)); + in = PyBytes_AsString(s); + size = PyBytes_GET_SIZE(s); + } + + // Prepare output buffer (str, out) + PyObject *str = PyBytes_FromStringAndSize((char *) NULL, size*2+3); if (!str) { Py_DECREF(s); return PyErr_NoMemory(); } - out = PyBytes_AS_STRING(str); + char *out = PyBytes_AS_STRING(str); + + // escape + unsigned long len; if (self && self->open) { #if MYSQL_VERSION_ID >= 50707 && !defined(MARIADB_BASE_VERSION) && !defined(MARIADB_VERSION_ID) len = mysql_real_escape_string_quote(&(self->connection), out+1, in, size, '\''); @@ -1018,10 +1035,14 @@ _mysql_string_literal( } else { len = mysql_escape_string(out+1, in, size); } - *out = *(out+len+1) = '\''; - if (_PyBytes_Resize(&str, len+2) < 0) return NULL; + Py_DECREF(s); - return (str); + *out = *(out+len+1) = '\''; + if (_PyBytes_Resize(&str, len+2) < 0) { + Py_DECREF(str); + return NULL; + } + return str; } static PyObject * @@ -1499,8 +1520,9 @@ _mysql_ResultObject_discard( // do nothing } Py_END_ALLOW_THREADS - if (mysql_errno(self->conn)) { - return _mysql_Exception(self->conn); + _mysql_ConnectionObject *conn = (_mysql_ConnectionObject *)self->conn; + if (mysql_errno(&conn->connection)) { + return _mysql_Exception(conn); } Py_RETURN_NONE; } From ba859845051680031153735f0bb1fedbf7f0a302 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Thu, 18 May 2023 20:21:11 +0900 Subject: [PATCH 36/39] Update README.md --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 23db1f27..d8ed79ca 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Install MySQL and mysqlclient: ``` # Assume you are activating Python 3 venv -$ brew install mysql +$ brew install mysql pkg-config $ pip install mysqlclient ``` @@ -58,9 +58,8 @@ 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" +$ brew install mysql-client pkg-config +$ export PKG_CONFIG_PATH="/opt/homebrew/opt/mysql-client/lib/pkgconfig" $ pip install mysqlclient ``` From 398208f8c8322bc3604801ae26c4f7d742651b20 Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Thu, 18 May 2023 13:11:13 -0400 Subject: [PATCH 37/39] Fix mariadbclient SSL support (#609) --- src/MySQLdb/_mysql.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/MySQLdb/_mysql.c b/src/MySQLdb/_mysql.c index 1f52d90b..cc419776 100644 --- a/src/MySQLdb/_mysql.c +++ b/src/MySQLdb/_mysql.c @@ -531,10 +531,11 @@ _mysql_ConnectionObject_Initialize( // See https://github.com/PyMySQL/mysqlclient/issues/474 // TODO: Does MariaDB supports PREFERRED and VERIFY_CA? // We support only two levels for now. - if (sslmode_num >= SSLMODE_REQUIRED) { + my_bool enforce_tls = 1; + if (ssl_mode_num >= SSLMODE_REQUIRED) { mysql_optionsv(&(self->connection), MYSQL_OPT_SSL_ENFORCE, (void *)&enforce_tls); } - if (sslmode_num >= SSLMODE_VERIFY_CA) { + if (ssl_mode_num >= SSLMODE_VERIFY_CA) { mysql_optionsv(&(self->connection), MYSQL_OPT_SSL_VERIFY_SERVER_CERT, (void *)&enforce_tls); } #endif From 5dfab4d9c5c5bc141eb66ea761efe7a7b51e1956 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Fri, 19 May 2023 03:04:56 +0900 Subject: [PATCH 38/39] CI: Update Django test workflow (#610) --- .github/workflows/django.yaml | 32 ++++++++++++++++++++------------ Makefile | 4 ++-- ci/test_mysql.py | 6 ++++-- src/MySQLdb/cursors.py | 10 +++++++--- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/.github/workflows/django.yaml b/.github/workflows/django.yaml index 737f34be..a7e076ce 100644 --- a/.github/workflows/django.yaml +++ b/.github/workflows/django.yaml @@ -2,15 +2,22 @@ name: Django compat test on: push: + pull_request: jobs: build: + name: "Run Django LTS test suite" runs-on: ubuntu-latest + env: + PIP_NO_PYTHON_VERSION_WARNING: 1 + PIP_DISABLE_PIP_VERSION_CHECK: 1 + DJANGO_VERSION: "3.2.19" steps: - name: Start MySQL run: | sudo systemctl start mysql.service mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -uroot -proot mysql + mysql -uroot -proot -e "set global innodb_flush_log_at_trx_commit=0;" mysql -uroot -proot -e "CREATE USER 'scott'@'%' IDENTIFIED BY 'tiger'; GRANT ALL ON *.* TO scott;" mysql -uroot -proot -e "CREATE DATABASE django_default; CREATE DATABASE django_other;" @@ -19,27 +26,28 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - # https://www.mail-archive.com/django-updates@googlegroups.com/msg209056.html - python-version: "3.11" + # Django 3.2.9+ supports Python 3.10 + # https://docs.djangoproject.com/ja/3.2/releases/3.2/ + python-version: "3.10" + cache: "pip" + cache-dependency-path: "ci/django-requirements.txt" - name: Install mysqlclient - env: - PIP_NO_PYTHON_VERSION_WARNING: 1 - PIP_DISABLE_PIP_VERSION_CHECK: 1 run: | - pip install -r requirements.txt + #pip install -r requirements.txt + #pip install mysqlclient # Use stable version pip install . - # pip install mysqlclient # Use stable version - - name: Run Django test - env: - DJANGO_VERSION: "3.2.19" + - name: Setup Django run: | sudo apt-get install libmemcached-dev wget https://github.com/django/django/archive/${DJANGO_VERSION}.tar.gz tar xf ${DJANGO_VERSION}.tar.gz cp ci/test_mysql.py django-${DJANGO_VERSION}/tests/ + cd django-${DJANGO_VERSION} + pip install . -r tests/requirements/py3.txt + + - name: Run Django test + run: | cd django-${DJANGO_VERSION}/tests/ - pip install .. - pip install -r requirements/py3.txt PYTHONPATH=.. python3 ./runtests.py --settings=test_mysql diff --git a/Makefile b/Makefile index f0e94c3b..bcd4334d 100644 --- a/Makefile +++ b/Makefile @@ -17,5 +17,5 @@ clean: .PHONY: check check: - ruff . - black *.py src + ruff *.py src ci + black *.py src ci diff --git a/ci/test_mysql.py b/ci/test_mysql.py index e285f4cf..9417fc9f 100644 --- a/ci/test_mysql.py +++ b/ci/test_mysql.py @@ -19,7 +19,7 @@ "HOST": "127.0.0.1", "USER": "scott", "PASSWORD": "tiger", - "TEST": {"CHARSET": "utf8mb4", "COLLATION": "utf8mb4_general_ci"}, + "TEST": {"CHARSET": "utf8mb3", "COLLATION": "utf8mb3_general_ci"}, }, "other": { "ENGINE": "django.db.backends.mysql", @@ -27,7 +27,7 @@ "HOST": "127.0.0.1", "USER": "scott", "PASSWORD": "tiger", - "TEST": {"CHARSET": "utf8mb4", "COLLATION": "utf8mb4_general_ci"}, + "TEST": {"CHARSET": "utf8mb3", "COLLATION": "utf8mb3_general_ci"}, }, } @@ -37,3 +37,5 @@ PASSWORD_HASHERS = [ "django.contrib.auth.hashers.MD5PasswordHasher", ] + +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" diff --git a/src/MySQLdb/cursors.py b/src/MySQLdb/cursors.py index 785fa9a1..70fbeea4 100644 --- a/src/MySQLdb/cursors.py +++ b/src/MySQLdb/cursors.py @@ -66,7 +66,7 @@ def __init__(self, connection): self.connection = connection self.description = None self.description_flags = None - self.rowcount = -1 + self.rowcount = 0 self.arraysize = 1 self._executed = None @@ -78,8 +78,10 @@ def __init__(self, connection): def _discard(self): self.description = None self.description_flags = None - self.rowcount = -1 - self.lastrowid = None + # Django uses some member after __exit__. + # So we keep rowcount and lastrowid here. They are cleared in Cursor._query(). + # self.rowcount = 0 + # self.lastrowid = None self._rows = None self.rownumber = None @@ -323,6 +325,8 @@ def callproc(self, procname, args=()): def _query(self, q): db = self._get_db() self._result = None + self.rowcount = None + self.lastrowid = None db.query(q) self._do_get_result(db) self._post_get_result() From 640fe6de2a40f9e9600bad4e0be1465cae8f0f10 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Fri, 19 May 2023 13:20:33 +0900 Subject: [PATCH 39/39] Release v2.2.0rc1 (#607) --- HISTORY.rst | 13 +++++++++++-- src/MySQLdb/release.py | 4 ++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 13e5cb01..ac083156 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,8 +5,17 @@ Release: TBD * Use ``pkg-config`` instead of ``mysql_config`` (#586) - - +* Raise ProgrammingError on -inf (#557) +* Raise IntegrityError for ER_BAD_NULL. (#579) +* Windows: Use MariaDB Connector/C 3.3.4 (#585) +* Use pkg-config instead of mysql_config (#586) +* Add collation option (#564) +* Drop Python 3.7 support (#593) +* Use pyproject.toml for build (#598) +* Add Cursor.mogrify (#477) +* Partial support of ssl_mode option with mariadbclient (#475) +* Discard remaining results without creating Python objects (#601) +* Fix executemany with binary prefix (#605) ====================== What's new in 2.1.1 diff --git a/src/MySQLdb/release.py b/src/MySQLdb/release.py index 55359628..08b9e619 100644 --- a/src/MySQLdb/release.py +++ b/src/MySQLdb/release.py @@ -1,3 +1,3 @@ __author__ = "Inada Naoki " -version_info = (2, 2, 0, "dev", 0) -__version__ = "2.2.0.dev0" +version_info = (2, 2, 0, "rc", 1) +__version__ = "2.2.0.rc1"