From a6ae2c71966fb65b071c9066e21d8c806df42f15 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 21 May 2024 20:04:54 +0900 Subject: [PATCH 01/21] fix format --- pymysql/tests/test_connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymysql/tests/test_connection.py b/pymysql/tests/test_connection.py index dcf3394c..d8e69b32 100644 --- a/pymysql/tests/test_connection.py +++ b/pymysql/tests/test_connection.py @@ -854,7 +854,7 @@ def test_escape_dict_raise_typeerror(self): mapping = con.encoders.copy() mapping[Foo] = escape_foo - #self.assertEqual(con.escape({"foo": Foo()}, mapping), {"foo": "bar"}) + # self.assertEqual(con.escape({"foo": Foo()}, mapping), {"foo": "bar"}) with self.assertRaises(TypeError): con.escape({"foo": Foo()}) From 53b35f7fbe6d0e4cfc22996ab9f5523a4829b11c Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 21 May 2024 20:09:01 +0900 Subject: [PATCH 02/21] update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 825dc47c..a633f6c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Release date: 2024-05-21 * Prohibit dict parameter for `Cursor.execute()`. It didn't produce valid SQL and might cause SQL injection. (CVE-2024-36039) +* Added ssl_key_password param. #1145 ## v1.1.0 From 95635f587ba9076e71a223b113efb08ac34a361d Mon Sep 17 00:00:00 2001 From: Mirko Palancaji <41736842+palm002@users.noreply.github.com> Date: Wed, 19 Jun 2024 22:57:53 +1000 Subject: [PATCH 03/21] Prevent UnboundLocalError during unbuffered query (#1174) Addresses the issue of `UnboundLocalError` which occurs when `MySQLResult` class fails to initialize due to a `SystemExit` exception by initialising the `MySQLResult` object before `try/except` block. Co-authored-by: Inada Naoki --- pymysql/connections.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/pymysql/connections.py b/pymysql/connections.py index 3a04ddd6..f12731e1 100644 --- a/pymysql/connections.py +++ b/pymysql/connections.py @@ -812,16 +812,10 @@ def _write_bytes(self, data): def _read_query_result(self, unbuffered=False): self._result = None + result = MySQLResult(self) if unbuffered: - try: - result = MySQLResult(self) - result.init_unbuffered_query() - except: - result.unbuffered_active = False - result.connection = None - raise + result.init_unbuffered_query() else: - result = MySQLResult(self) result.read() self._result = result if result.server_status is not None: @@ -1212,17 +1206,16 @@ def init_unbuffered_query(self): :raise OperationalError: If the connection to the MySQL server is lost. :raise InternalError: """ - self.unbuffered_active = True first_packet = self.connection._read_packet() if first_packet.is_ok_packet(): - self._read_ok_packet(first_packet) - self.unbuffered_active = False self.connection = None + self._read_ok_packet(first_packet) elif first_packet.is_load_local_packet(): - self._read_load_local_packet(first_packet) - self.unbuffered_active = False - self.connection = None + try: + self._read_load_local_packet(first_packet) + finally: + self.connection = None else: self.field_count = first_packet.read_length_encoded_integer() self._get_descriptions() @@ -1231,6 +1224,7 @@ def init_unbuffered_query(self): # value of a 64bit unsigned integer. Since we're emulating MySQLdb, # we set it to this instead of None, which would be preferred. self.affected_rows = 18446744073709551615 + self.unbuffered_active = True def _read_ok_packet(self, first_packet): ok_packet = OKPacketWrapper(first_packet) From d93cde99055092b9c802a5038cf31bf98b2b87aa Mon Sep 17 00:00:00 2001 From: CF Bolz-Tereick Date: Wed, 4 Sep 2024 06:40:43 +0200 Subject: [PATCH 04/21] remove mention of runtests.py (#1182) --- docs/source/user/development.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/user/development.rst b/docs/source/user/development.rst index 1f8a2637..2d80a624 100644 --- a/docs/source/user/development.rst +++ b/docs/source/user/development.rst @@ -28,7 +28,7 @@ and edit the new file to match your MySQL configuration:: $ cp ci/database.json pymysql/tests/databases.json $ $EDITOR pymysql/tests/databases.json -To run all the tests, execute the script ``runtests.py``:: +To run all the tests, you can use pytest:: - $ pip install pytest + $ pip install -r requirements-dev.txt $ pytest -v pymysql From 9204b641f3ecff73704e10549f615d8762358652 Mon Sep 17 00:00:00 2001 From: CF Bolz-Tereick Date: Thu, 5 Sep 2024 07:20:39 +0200 Subject: [PATCH 05/21] close `connection._rfile` in `Connection._force_close` (#1184) fix #1183. --- pymysql/connections.py | 10 ++++----- pymysql/tests/test_connection.py | 37 ++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/pymysql/connections.py b/pymysql/connections.py index f12731e1..5f60377e 100644 --- a/pymysql/connections.py +++ b/pymysql/connections.py @@ -161,6 +161,7 @@ class Connection: """ _sock = None + _rfile = None _auth_plugin_name = "" _closed = False _secure = False @@ -430,6 +431,8 @@ def open(self): def _force_close(self): """Close connection without QUIT message.""" + if self._rfile: + self._rfile.close() if self._sock: try: self._sock.close() @@ -696,12 +699,7 @@ def connect(self, sock=None): if self.autocommit_mode is not None: self.autocommit(self.autocommit_mode) except BaseException as e: - self._rfile = None - if sock is not None: - try: - sock.close() - except: # noqa - pass + self._force_close() if isinstance(e, (OSError, IOError)): exc = err.OperationalError( diff --git a/pymysql/tests/test_connection.py b/pymysql/tests/test_connection.py index d8e69b32..61dba600 100644 --- a/pymysql/tests/test_connection.py +++ b/pymysql/tests/test_connection.py @@ -883,3 +883,40 @@ def test_commit_during_multi_result(self): con.commit() cur.execute("SELECT 3") self.assertEqual(cur.fetchone()[0], 3) + + def test_force_close_closes_socketio(self): + con = self.connect() + sock = con._sock + fileno = sock.fileno() + rfile = con._rfile + + con._force_close() + assert rfile.closed + assert sock._closed + assert sock.fileno() != fileno # should be set to -1 + + def test_socket_closed_on_exception_in_connect(self): + con = self.connect(defer_connect=True) + sock = None + rfile = None + fileno = -1 + + def _request_authentication(): + nonlocal sock, rfile, fileno + sock = con._sock + assert sock is not None + fileno = sock.fileno() + rfile = con._rfile + assert rfile is not None + raise TypeError + + con._request_authentication = _request_authentication + + with pytest.raises(TypeError): + con.connect() + assert not con.open + assert con._rfile is None + assert con._sock is None + assert rfile.closed + assert sock._closed + assert sock.fileno() != fileno # should be set to -1 From ec27bade879ad05fda214188d035c1fe3f255a35 Mon Sep 17 00:00:00 2001 From: Ujjwal Kumar Singh <95489300+theneuralcraftsman@users.noreply.github.com> Date: Wed, 18 Sep 2024 13:21:03 +0530 Subject: [PATCH 06/21] Added MariaDB in readme description (#1186) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 32f5df2f..a91c6008 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # PyMySQL -This package contains a pure-Python MySQL client library, based on [PEP +This package contains a pure-Python MySQL and MariaDB client library, based on [PEP 249](https://www.python.org/dev/peps/pep-0249/). ## Requirements From 54e68807dd1a3f67b855c1e8c4c6ce0526d2bff1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 18:23:07 +0900 Subject: [PATCH 07/21] chore(deps): update dependency sphinx-rtd-theme to v3 (#1189) --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 01406623..d2f5c5a5 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,2 @@ sphinx~=7.2 -sphinx-rtd-theme~=2.0.0 +sphinx-rtd-theme~=3.0.0 From dabf0982b498112db8883dcf71a4f68c9d2d9fad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 18:30:20 +0900 Subject: [PATCH 08/21] chore(deps): update dependency sphinx to v8 (#1179) --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index d2f5c5a5..48319f03 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,2 @@ -sphinx~=7.2 +sphinx~=8.0 sphinx-rtd-theme~=3.0.0 From a1ac8239c8bf79e7f1a17347b10d6e184221f9c1 Mon Sep 17 00:00:00 2001 From: Cycloctane Date: Wed, 6 Nov 2024 11:46:44 +0800 Subject: [PATCH 09/21] Add support for Python 3.13 (#1190) - fixes #1188 - Add python 3.13 to test matrix and pyproject.toml --- .github/workflows/test.yaml | 3 +++ pymysql/connections.py | 6 ++++-- pyproject.toml | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6d59d8c4..d3693fdd 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -30,6 +30,9 @@ jobs: - db: "mariadb:10.6" py: "3.12" + - db: "mariadb:10.6" + py: "3.13" + - db: "mariadb:lts" py: "3.9" diff --git a/pymysql/connections.py b/pymysql/connections.py index 5f60377e..fe4d0c45 100644 --- a/pymysql/connections.py +++ b/pymysql/connections.py @@ -40,8 +40,10 @@ DEFAULT_USER = getpass.getuser() del getpass -except (ImportError, KeyError): - # KeyError occurs when there's no entry in OS database for a current user. +except (ImportError, KeyError, OSError): + # When there's no entry in OS database for a current user: + # KeyError is raised in Python 3.12 and below. + # OSError is raised in Python 3.13+ DEFAULT_USER = None DEBUG = False diff --git a/pyproject.toml b/pyproject.toml index 8cd9ddb4..ee103916 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Intended Audience :: Developers", From 8876b98b683912b46ddafa1ac2fcea9911e2c8c4 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Mon, 2 Dec 2024 18:37:23 +0900 Subject: [PATCH 10/21] ci: remove lock-threads --- .github/workflows/lock.yml | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 .github/workflows/lock.yml diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml deleted file mode 100644 index 21449e3b..00000000 --- a/.github/workflows/lock.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: 'Lock Threads' - -on: - schedule: - - cron: '30 9 * * 1' - -permissions: - issues: write - pull-requests: write - -jobs: - lock-threads: - if: github.repository == 'PyMySQL/PyMySQL' - runs-on: ubuntu-latest - steps: - - uses: dessant/lock-threads@v5 - From 7dead51f8605f315e7931bae58ea8b2126b945ba Mon Sep 17 00:00:00 2001 From: Eugene Kennedy Date: Sun, 12 Jan 2025 03:17:12 -0500 Subject: [PATCH 11/21] Resolve UTF8 charset case-insensitively (#1195) --- pymysql/charset.py | 3 ++- pymysql/tests/test_charset.py | 22 ++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/pymysql/charset.py b/pymysql/charset.py index b1c1ca8b..ec8e14e2 100644 --- a/pymysql/charset.py +++ b/pymysql/charset.py @@ -45,9 +45,10 @@ def by_id(self, id): return self._by_id[id] def by_name(self, name): + name = name.lower() if name == "utf8": name = "utf8mb4" - return self._by_name.get(name.lower()) + return self._by_name.get(name) _charsets = Charsets() diff --git a/pymysql/tests/test_charset.py b/pymysql/tests/test_charset.py index 94e6e155..85a310e4 100644 --- a/pymysql/tests/test_charset.py +++ b/pymysql/tests/test_charset.py @@ -21,5 +21,23 @@ def test_utf8(): ) # utf8 is alias of utf8mb4 since MySQL 8.0, and PyMySQL v1.1. - utf8 = pymysql.charset.charset_by_name("utf8") - assert utf8 == utf8mb4 + lowercase_utf8 = pymysql.charset.charset_by_name("utf8") + assert lowercase_utf8 == utf8mb4 + + # Regardless of case, UTF8 (which is special cased) should resolve to the same thing + uppercase_utf8 = pymysql.charset.charset_by_name("UTF8") + mixedcase_utf8 = pymysql.charset.charset_by_name("UtF8") + assert uppercase_utf8 == lowercase_utf8 + assert mixedcase_utf8 == lowercase_utf8 + +def test_case_sensitivity(): + lowercase_latin1 = pymysql.charset.charset_by_name("latin1") + assert lowercase_latin1 is not None + + # lowercase and uppercase should resolve to the same charset + uppercase_latin1 = pymysql.charset.charset_by_name("LATIN1") + assert uppercase_latin1 == lowercase_latin1 + + # lowercase and mixed case should resolve to the same charset + mixedcase_latin1 = pymysql.charset.charset_by_name("LaTiN1") + assert mixedcase_latin1 == lowercase_latin1 From 046d36c83a272b322b41146a326af4606df9f0d4 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 14 Jan 2025 16:51:25 +0900 Subject: [PATCH 12/21] update ci versions (#1196) --- .github/workflows/test.yaml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d3693fdd..b67c2ea9 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -22,14 +22,11 @@ jobs: py: "3.8" - db: "mariadb:10.5" - py: "3.7" + py: "3.8" - db: "mariadb:10.6" py: "3.11" - - db: "mariadb:10.6" - py: "3.12" - - db: "mariadb:10.6" py: "3.13" @@ -37,14 +34,15 @@ jobs: py: "3.9" - db: "mysql:5.7" - py: "pypy-3.8" + py: "pypy-3.10" - db: "mysql:8.0" - py: "3.9" + py: "3.8" mysql_auth: true - db: "mysql:8.0" py: "3.10" + mysql_auth: true services: mysql: From 0d4609c22b55ad7827ab7186cbbc44068f0a0ed2 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 14 Jan 2025 19:05:42 +0900 Subject: [PATCH 13/21] use KILL instead of COM_KILL for MySQL 8.4 support (#1197) --- .github/workflows/test.yaml | 16 ++++++++-------- pymysql/connections.py | 6 +++--- pymysql/tests/test_charset.py | 1 + 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b67c2ea9..a8e10af0 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,29 +19,29 @@ jobs: matrix: include: - db: "mariadb:10.4" - py: "3.8" + py: "3.13" - db: "mariadb:10.5" - py: "3.8" + py: "3.11" - db: "mariadb:10.6" - py: "3.11" + py: "3.10" - db: "mariadb:10.6" - py: "3.13" + py: "3.9" - db: "mariadb:lts" - py: "3.9" + py: "3.8" - db: "mysql:5.7" py: "pypy-3.10" - db: "mysql:8.0" - py: "3.8" + py: "3.13" mysql_auth: true - - db: "mysql:8.0" - py: "3.10" + - db: "mysql:8.4" + py: "3.8" mysql_auth: true services: diff --git a/pymysql/connections.py b/pymysql/connections.py index fe4d0c45..91825f75 100644 --- a/pymysql/connections.py +++ b/pymysql/connections.py @@ -576,9 +576,9 @@ def affected_rows(self): return self._affected_rows def kill(self, thread_id): - arg = struct.pack(" Date: Tue, 14 Jan 2025 19:36:35 +0900 Subject: [PATCH 14/21] disable VERIFY_X509_STRICT for Python 3.13 support (#1198) --- pymysql/connections.py | 6 ++++++ pymysql/tests/test_connection.py | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/pymysql/connections.py b/pymysql/connections.py index 91825f75..2ddcb3f7 100644 --- a/pymysql/connections.py +++ b/pymysql/connections.py @@ -377,6 +377,12 @@ def _create_ssl_ctx(self, sslp): capath = sslp.get("capath") hasnoca = ca is None and capath is None ctx = ssl.create_default_context(cafile=ca, capath=capath) + + # Python 3.13 enables VERIFY_X509_STRICT by default. + # But self signed certificates that are generated by MySQL automatically + # doesn't pass the verification. + ctx.verify_flags &= ~ssl.VERIFY_X509_STRICT + ctx.check_hostname = not hasnoca and sslp.get("check_hostname", True) verify_mode_value = sslp.get("verify_mode") if verify_mode_value is None: diff --git a/pymysql/tests/test_connection.py b/pymysql/tests/test_connection.py index 61dba600..03e35f86 100644 --- a/pymysql/tests/test_connection.py +++ b/pymysql/tests/test_connection.py @@ -558,7 +558,7 @@ def test_defer_connect(self): sock.close() def test_ssl_connect(self): - dummy_ssl_context = mock.Mock(options=0) + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) with mock.patch("pymysql.connections.Connection.connect"), mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), @@ -581,7 +581,7 @@ def test_ssl_connect(self): ) dummy_ssl_context.set_ciphers.assert_called_with("cipher") - dummy_ssl_context = mock.Mock(options=0) + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) with mock.patch("pymysql.connections.Connection.connect"), mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), @@ -603,7 +603,7 @@ def test_ssl_connect(self): ) dummy_ssl_context.set_ciphers.assert_not_called - dummy_ssl_context = mock.Mock(options=0) + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) with mock.patch("pymysql.connections.Connection.connect"), mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), @@ -626,7 +626,7 @@ def test_ssl_connect(self): ) dummy_ssl_context.set_ciphers.assert_not_called - dummy_ssl_context = mock.Mock(options=0) + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) with mock.patch("pymysql.connections.Connection.connect"), mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), @@ -640,7 +640,7 @@ def test_ssl_connect(self): dummy_ssl_context.load_cert_chain.assert_not_called dummy_ssl_context.set_ciphers.assert_not_called - dummy_ssl_context = mock.Mock(options=0) + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) with mock.patch("pymysql.connections.Connection.connect"), mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), @@ -661,7 +661,7 @@ def test_ssl_connect(self): dummy_ssl_context.set_ciphers.assert_not_called for ssl_verify_cert in (True, "1", "yes", "true"): - dummy_ssl_context = mock.Mock(options=0) + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) with mock.patch("pymysql.connections.Connection.connect"), mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), @@ -682,7 +682,7 @@ def test_ssl_connect(self): dummy_ssl_context.set_ciphers.assert_not_called for ssl_verify_cert in (None, False, "0", "no", "false"): - dummy_ssl_context = mock.Mock(options=0) + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) with mock.patch("pymysql.connections.Connection.connect"), mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), @@ -704,7 +704,7 @@ def test_ssl_connect(self): for ssl_ca in ("ca", None): for ssl_verify_cert in ("foo", "bar", ""): - dummy_ssl_context = mock.Mock(options=0) + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) with mock.patch("pymysql.connections.Connection.connect"), mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), @@ -727,7 +727,7 @@ def test_ssl_connect(self): ) dummy_ssl_context.set_ciphers.assert_not_called - dummy_ssl_context = mock.Mock(options=0) + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) with mock.patch("pymysql.connections.Connection.connect"), mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), @@ -748,7 +748,7 @@ def test_ssl_connect(self): ) dummy_ssl_context.set_ciphers.assert_not_called - dummy_ssl_context = mock.Mock(options=0) + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) with mock.patch("pymysql.connections.Connection.connect"), mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), @@ -770,7 +770,7 @@ def test_ssl_connect(self): ) dummy_ssl_context.set_ciphers.assert_not_called - dummy_ssl_context = mock.Mock(options=0) + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) with mock.patch("pymysql.connections.Connection.connect"), mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), @@ -785,7 +785,7 @@ def test_ssl_connect(self): ) assert not create_default_context.called - dummy_ssl_context = mock.Mock(options=0) + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) with mock.patch("pymysql.connections.Connection.connect"), mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), From 1920de3d8eca0565979e6c32dc2fdfd29c3d8db4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Jan 2025 19:40:04 +0900 Subject: [PATCH 15/21] chore(deps): update codecov/codecov-action action to v5 (#1191) --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a8e10af0..6abc96b7 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -107,4 +107,4 @@ jobs: - name: Upload coverage reports to Codecov if: github.repository == 'PyMySQL/PyMySQL' - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 From 66ad1eaa47cfde19dfe01900ceb5f6ea51483e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sil=C3=A9n?= Date: Tue, 14 Jan 2025 12:44:46 +0200 Subject: [PATCH 16/21] add MariaDB to README.md (#1181) --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a91c6008..95e4520a 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@ # PyMySQL -This package contains a pure-Python MySQL and MariaDB client library, based on [PEP -249](https://www.python.org/dev/peps/pep-0249/). +This package contains a pure-Python MySQL and MariaDB client library, based on +[PEP 249](https://www.python.org/dev/peps/pep-0249/). ## Requirements @@ -92,6 +92,7 @@ This example will print: - DB-API 2.0: - MySQL Reference Manuals: +- Getting Help With MariaDB - MySQL client/server protocol: - "Connector" channel in MySQL Community Slack: From 5f6533f883535b76c2d3a776c4746027027106f8 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 15 Jan 2025 10:45:47 +0900 Subject: [PATCH 17/21] refactor: use defer_connect instead of mock (#1199) --- pymysql/tests/test_connection.py | 36 +++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/pymysql/tests/test_connection.py b/pymysql/tests/test_connection.py index 03e35f86..1a16c982 100644 --- a/pymysql/tests/test_connection.py +++ b/pymysql/tests/test_connection.py @@ -559,7 +559,7 @@ def test_defer_connect(self): def test_ssl_connect(self): dummy_ssl_context = mock.Mock(options=0, verify_flags=0) - with mock.patch("pymysql.connections.Connection.connect"), mock.patch( + with mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), ) as create_default_context: @@ -570,6 +570,7 @@ def test_ssl_connect(self): "key": "key", "cipher": "cipher", }, + defer_connect=True, ) assert create_default_context.called assert dummy_ssl_context.check_hostname @@ -582,7 +583,7 @@ def test_ssl_connect(self): dummy_ssl_context.set_ciphers.assert_called_with("cipher") dummy_ssl_context = mock.Mock(options=0, verify_flags=0) - with mock.patch("pymysql.connections.Connection.connect"), mock.patch( + with mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), ) as create_default_context: @@ -592,6 +593,7 @@ def test_ssl_connect(self): "cert": "cert", "key": "key", }, + defer_connect=True, ) assert create_default_context.called assert dummy_ssl_context.check_hostname @@ -604,7 +606,7 @@ def test_ssl_connect(self): dummy_ssl_context.set_ciphers.assert_not_called dummy_ssl_context = mock.Mock(options=0, verify_flags=0) - with mock.patch("pymysql.connections.Connection.connect"), mock.patch( + with mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), ) as create_default_context: @@ -615,6 +617,7 @@ def test_ssl_connect(self): "key": "key", "password": "password", }, + defer_connect=True, ) assert create_default_context.called assert dummy_ssl_context.check_hostname @@ -627,12 +630,13 @@ def test_ssl_connect(self): dummy_ssl_context.set_ciphers.assert_not_called dummy_ssl_context = mock.Mock(options=0, verify_flags=0) - with mock.patch("pymysql.connections.Connection.connect"), mock.patch( + with mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), ) as create_default_context: pymysql.connect( ssl_ca="ca", + defer_connect=True, ) assert create_default_context.called assert not dummy_ssl_context.check_hostname @@ -641,7 +645,7 @@ def test_ssl_connect(self): dummy_ssl_context.set_ciphers.assert_not_called dummy_ssl_context = mock.Mock(options=0, verify_flags=0) - with mock.patch("pymysql.connections.Connection.connect"), mock.patch( + with mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), ) as create_default_context: @@ -649,6 +653,7 @@ def test_ssl_connect(self): ssl_ca="ca", ssl_cert="cert", ssl_key="key", + defer_connect=True, ) assert create_default_context.called assert not dummy_ssl_context.check_hostname @@ -662,7 +667,7 @@ def test_ssl_connect(self): for ssl_verify_cert in (True, "1", "yes", "true"): dummy_ssl_context = mock.Mock(options=0, verify_flags=0) - with mock.patch("pymysql.connections.Connection.connect"), mock.patch( + with mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), ) as create_default_context: @@ -670,6 +675,7 @@ def test_ssl_connect(self): ssl_cert="cert", ssl_key="key", ssl_verify_cert=ssl_verify_cert, + defer_connect=True, ) assert create_default_context.called assert not dummy_ssl_context.check_hostname @@ -683,7 +689,7 @@ def test_ssl_connect(self): for ssl_verify_cert in (None, False, "0", "no", "false"): dummy_ssl_context = mock.Mock(options=0, verify_flags=0) - with mock.patch("pymysql.connections.Connection.connect"), mock.patch( + with mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), ) as create_default_context: @@ -691,6 +697,7 @@ def test_ssl_connect(self): ssl_cert="cert", ssl_key="key", ssl_verify_cert=ssl_verify_cert, + defer_connect=True, ) assert create_default_context.called assert not dummy_ssl_context.check_hostname @@ -705,7 +712,7 @@ def test_ssl_connect(self): for ssl_ca in ("ca", None): for ssl_verify_cert in ("foo", "bar", ""): dummy_ssl_context = mock.Mock(options=0, verify_flags=0) - with mock.patch("pymysql.connections.Connection.connect"), mock.patch( + with mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), ) as create_default_context: @@ -714,6 +721,7 @@ def test_ssl_connect(self): ssl_cert="cert", ssl_key="key", ssl_verify_cert=ssl_verify_cert, + defer_connect=True, ) assert create_default_context.called assert not dummy_ssl_context.check_hostname @@ -728,7 +736,7 @@ def test_ssl_connect(self): dummy_ssl_context.set_ciphers.assert_not_called dummy_ssl_context = mock.Mock(options=0, verify_flags=0) - with mock.patch("pymysql.connections.Connection.connect"), mock.patch( + with mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), ) as create_default_context: @@ -737,6 +745,7 @@ def test_ssl_connect(self): ssl_cert="cert", ssl_key="key", ssl_verify_identity=True, + defer_connect=True, ) assert create_default_context.called assert dummy_ssl_context.check_hostname @@ -749,7 +758,7 @@ def test_ssl_connect(self): dummy_ssl_context.set_ciphers.assert_not_called dummy_ssl_context = mock.Mock(options=0, verify_flags=0) - with mock.patch("pymysql.connections.Connection.connect"), mock.patch( + with mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), ) as create_default_context: @@ -759,6 +768,7 @@ def test_ssl_connect(self): ssl_key="key", ssl_key_password="password", ssl_verify_identity=True, + defer_connect=True, ) assert create_default_context.called assert dummy_ssl_context.check_hostname @@ -771,7 +781,7 @@ def test_ssl_connect(self): dummy_ssl_context.set_ciphers.assert_not_called dummy_ssl_context = mock.Mock(options=0, verify_flags=0) - with mock.patch("pymysql.connections.Connection.connect"), mock.patch( + with mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), ) as create_default_context: @@ -782,11 +792,12 @@ def test_ssl_connect(self): "cert": "cert", "key": "key", }, + defer_connect=True, ) assert not create_default_context.called dummy_ssl_context = mock.Mock(options=0, verify_flags=0) - with mock.patch("pymysql.connections.Connection.connect"), mock.patch( + with mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), ) as create_default_context: @@ -795,6 +806,7 @@ def test_ssl_connect(self): ssl_ca="ca", ssl_cert="cert", ssl_key="key", + defer_connect=True, ) assert not create_default_context.called From e88b729f8f1ddcf0851e0153188b016d0e9ec00c Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 15 Jan 2025 11:43:46 +0900 Subject: [PATCH 18/21] remove codeql and codesee actions --- .github/workflows/codeql-analysis.yml | 62 ---------------------- .github/workflows/codesee-arch-diagram.yml | 23 -------- 2 files changed, 85 deletions(-) delete mode 100644 .github/workflows/codeql-analysis.yml delete mode 100644 .github/workflows/codesee-arch-diagram.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index df49979e..00000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,62 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ main ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ main ] - schedule: - - cron: '34 7 * * 2' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - - strategy: - fail-fast: false - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: "python" - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/codesee-arch-diagram.yml b/.github/workflows/codesee-arch-diagram.yml deleted file mode 100644 index 806d41d1..00000000 --- a/.github/workflows/codesee-arch-diagram.yml +++ /dev/null @@ -1,23 +0,0 @@ -# This workflow was added by CodeSee. Learn more at https://codesee.io/ -# This is v2.0 of this workflow file -on: - push: - branches: - - main - pull_request_target: - types: [opened, synchronize, reopened] - -name: CodeSee - -permissions: read-all - -jobs: - codesee: - runs-on: ubuntu-latest - continue-on-error: true - name: Analyze the repo with CodeSee - steps: - - uses: Codesee-io/codesee-action@v2 - with: - codesee-token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} - codesee-url: https://app.codesee.io From 53efd1ec7f0e854abc62eb875b944f56bca29dd2 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 29 Jan 2025 16:57:30 +0900 Subject: [PATCH 19/21] ci: use astral-sh/ruff-action (#1201) --- .github/workflows/lint.yaml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 269211c2..07ea6603 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -13,13 +13,12 @@ jobs: lint: runs-on: ubuntu-latest steps: - - name: checkout - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: lint - uses: chartboost/ruff-action@v1 + - uses: astral-sh/ruff-action@v3 + + - name: format + run: ruff format --diff - - name: check format - uses: chartboost/ruff-action@v1 - with: - args: "format --diff" + - name: lint + run: ruff check --diff From 01af30fea0880c3b72e6c7b3b05d66a8c28ced7a Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 29 Jan 2025 18:06:45 +0900 Subject: [PATCH 20/21] fix auth_switch_request handling (#1200) --- .coveragerc | 1 + pymysql/_auth.py | 8 ++++++-- pymysql/connections.py | 4 ++++ tests/test_auth.py | 28 +++++++++++++++++++++++++++- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/.coveragerc b/.coveragerc index a9ec9942..efa9a2ff 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,6 +2,7 @@ branch = True source = pymysql + tests omit = pymysql/tests/* pymysql/tests/thirdparty/test_MySQLdb/* diff --git a/pymysql/_auth.py b/pymysql/_auth.py index 8ce744fb..4790449b 100644 --- a/pymysql/_auth.py +++ b/pymysql/_auth.py @@ -166,6 +166,8 @@ def sha256_password_auth(conn, pkt): if pkt.is_auth_switch_request(): conn.salt = pkt.read_all() + if conn.salt.endswith(b"\0"): + conn.salt = conn.salt[:-1] if not conn.server_public_key and conn.password: # Request server public key if DEBUG: @@ -215,9 +217,11 @@ def caching_sha2_password_auth(conn, pkt): if pkt.is_auth_switch_request(): # Try from fast auth - if DEBUG: - print("caching sha2: Trying fast path") conn.salt = pkt.read_all() + if conn.salt.endswith(b"\0"): # str.removesuffix is available in 3.9 + conn.salt = conn.salt[:-1] + if DEBUG: + print(f"caching sha2: Trying fast path. salt={conn.salt.hex()!r}") scrambled = scramble_caching_sha2(conn.password, conn.salt) pkt = _roundtrip(conn, scrambled) # else: fast auth is tried in initial handshake diff --git a/pymysql/connections.py b/pymysql/connections.py index 2ddcb3f7..99fcfcd0 100644 --- a/pymysql/connections.py +++ b/pymysql/connections.py @@ -47,6 +47,7 @@ DEFAULT_USER = None DEBUG = False +_DEFAULT_AUTH_PLUGIN = None # if this is not None, use it instead of server's default. TEXT_TYPES = { FIELD_TYPE.BIT, @@ -1158,6 +1159,9 @@ def _get_server_information(self): else: self._auth_plugin_name = data[i:server_end].decode("utf-8") + if _DEFAULT_AUTH_PLUGIN is not None: # for tests + self._auth_plugin_name = _DEFAULT_AUTH_PLUGIN + def get_server_info(self): return self.server_version diff --git a/tests/test_auth.py b/tests/test_auth.py index e5e2a64e..d7a0e82f 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -71,6 +71,19 @@ def test_caching_sha2_password(): con.query("FLUSH PRIVILEGES") con.close() + # Fast path after auth_switch_request + pymysql.connections._DEFAULT_AUTH_PLUGIN = "mysql_native_password" + con = pymysql.connect( + user="user_caching_sha2", + password=pass_caching_sha2, + host=host, + port=port, + ssl=ssl, + ) + con.query("FLUSH PRIVILEGES") + con.close() + pymysql.connections._DEFAULT_AUTH_PLUGIN = None + def test_caching_sha2_password_ssl(): con = pymysql.connect( @@ -88,7 +101,20 @@ def test_caching_sha2_password_ssl(): password=pass_caching_sha2, host=host, port=port, - ssl=None, + ssl=ssl, + ) + con.query("FLUSH PRIVILEGES") + con.close() + + # Fast path after auth_switch_request + pymysql.connections._DEFAULT_AUTH_PLUGIN = "mysql_native_password" + con = pymysql.connect( + user="user_caching_sha2", + password=pass_caching_sha2, + host=host, + port=port, + ssl=ssl, ) con.query("FLUSH PRIVILEGES") con.close() + pymysql.connections._DEFAULT_AUTH_PLUGIN = None From bed601f04fb982c7c42c6ff7e56b7f749ac64cc9 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 21 May 2025 18:36:16 +0900 Subject: [PATCH 21/21] Add DeepWiki badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 95e4520a..7d8bfc86 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ [![Documentation Status](https://readthedocs.org/projects/pymysql/badge/?version=latest)](https://pymysql.readthedocs.io/) [![codecov](https://codecov.io/gh/PyMySQL/PyMySQL/branch/main/graph/badge.svg?token=ppEuaNXBW4)](https://codecov.io/gh/PyMySQL/PyMySQL) +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/PyMySQL/PyMySQL) # PyMySQL