From 57e2e93276c7b48e6ec5b99c1712e48661d92183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 28 Mar 2023 16:48:57 +0200 Subject: [PATCH 01/80] Remove redundant wheel dep from pyproject.toml (#1099) Remove the redundant `wheel` dependency, as it is added by the backend automatically. Listing it explicitly in the documentation was a historical mistake and has been fixed since, see: https://github.com/pypa/setuptools/commit/f7d30a9529378cf69054b5176249e5457aaf640a --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0f043181..a67031b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ dynamic = ["version"] "Documentation" = "https://pymysql.readthedocs.io/" [build-system] -requires = ["setuptools>=61", "wheel"] +requires = ["setuptools>=61"] build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] From 885841f3fee416c222a75d83a81f74d3dcd71b51 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Fri, 31 Mar 2023 23:42:11 +0900 Subject: [PATCH 02/80] Add security policy --- SECURITY.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..da9c516d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +## Security contact information + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. From 72e7c580515588f0646c3322c3dba63dbcc90810 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Mon, 1 May 2023 19:22:22 +0900 Subject: [PATCH 03/80] Run lock-threads only on PyMySQL/PyMySQL --- .github/workflows/lock.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 5dde1354..780dd92d 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -9,7 +9,8 @@ permissions: pull-requests: write jobs: - action: + lock-threads: + if: github.repository == 'PyMySQL/PyMySQL' runs-on: ubuntu-latest steps: - uses: dessant/lock-threads@v4 From 101f6e970cb2df47f1363bca590aab88a809804c Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Mon, 22 May 2023 10:44:35 +0000 Subject: [PATCH 04/80] Update FUNDING.yml --- .github/FUNDING.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 89fc5cf8..253a13ac 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,10 +1,10 @@ # These are supported funding model platforms -github: [methane] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +github: ["methane"] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +tidelift: "pypi/PyMySQL" # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username From a5e837f9de3b13abcef3500a1dc35fdbfa2f5784 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 23 May 2023 19:28:20 +0900 Subject: [PATCH 05/80] ci: Fix black options (#1109) --- .github/workflows/lint.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index a3131ce2..9d9eafb0 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -18,7 +18,8 @@ jobs: python-version: 3.x - uses: psf/black@stable with: - args: ". --diff --check" + options: "--check --verbose" + src: "." - name: Setup flake8 annotations uses: rbialon/flake8-annotations@v1 - name: flake8 From 1448310e1400a87267f2707eadceab00af4dedad Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 23 May 2023 19:28:34 +0900 Subject: [PATCH 06/80] Remove unused function (#1108) --- pymysql/cursors.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pymysql/cursors.py b/pymysql/cursors.py index 2b5ccca9..b36f473c 100644 --- a/pymysql/cursors.py +++ b/pymysql/cursors.py @@ -95,13 +95,6 @@ def _nextset(self, unbuffered=False): def nextset(self): return self._nextset(False) - def _ensure_bytes(self, x, encoding=None): - if isinstance(x, str): - x = x.encode(encoding) - elif isinstance(x, (tuple, list)): - x = type(x)(self._ensure_bytes(v, encoding=encoding) for v in x) - return x - def _escape_args(self, args, conn): if isinstance(args, (tuple, list)): return tuple(conn.literal(arg) for arg in args) From 01ddf9d1b26d78d5d03e483d076544a5a50d7c47 Mon Sep 17 00:00:00 2001 From: Richard Schwab Date: Tue, 23 May 2023 14:01:02 +0200 Subject: [PATCH 07/80] Expose `Cursor.warning_count` (#1056) In #774 automatic warnings were removed. This provides a way to check for existence of warnings without having to perform an additional query over the network. Co-authored-by: Inada Naoki --- CHANGELOG.md | 7 +++++++ pymysql/cursors.py | 5 +++++ pymysql/tests/test_SSCursor.py | 33 ++++++++++++++++++++++++++++++-- pymysql/tests/test_cursor.py | 20 +++++++++++++++++-- pymysql/tests/test_load_local.py | 32 +++++++++++++++++++++++++++++++ 5 files changed, 93 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87c3f9e8..76fdb6a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changes +## v1.1.0 + +Release date: TBD + +* Exposed `Cursor.warning_count` to check for warnings without additional query (#1056) + ## v1.0.3 Release date: TBD @@ -7,6 +13,7 @@ Release date: TBD * Dropped support of end of life MySQL version 5.6 * Dropped support of end of life MariaDB versions below 10.3 * Dropped support of end of life Python version 3.6 +* Exposed `Cursor.warning_count` to check for warnings without additional query (#1056) ## v1.0.2 diff --git a/pymysql/cursors.py b/pymysql/cursors.py index b36f473c..e57fba76 100644 --- a/pymysql/cursors.py +++ b/pymysql/cursors.py @@ -32,6 +32,7 @@ class Cursor: def __init__(self, connection): self.connection = connection + self.warning_count = 0 self.description = None self.rownumber = 0 self.rowcount = -1 @@ -324,6 +325,7 @@ def _clear_result(self): self._result = None self.rowcount = 0 + self.warning_count = 0 self.description = None self.lastrowid = None self._rows = None @@ -334,6 +336,7 @@ def _do_get_result(self): self._result = result = conn._result self.rowcount = result.affected_rows + self.warning_count = result.warning_count self.description = result.description self.lastrowid = result.insert_id self._rows = result.rows @@ -435,6 +438,7 @@ def fetchone(self): self._check_executed() row = self.read_next() if row is None: + self.warning_count = self._result.warning_count return None self.rownumber += 1 return row @@ -468,6 +472,7 @@ def fetchmany(self, size=None): for i in range(size): row = self.read_next() if row is None: + self.warning_count = self._result.warning_count break rows.append(row) self.rownumber += 1 diff --git a/pymysql/tests/test_SSCursor.py b/pymysql/tests/test_SSCursor.py index a68a7769..d19d3e5d 100644 --- a/pymysql/tests/test_SSCursor.py +++ b/pymysql/tests/test_SSCursor.py @@ -3,13 +3,13 @@ try: from pymysql.tests import base import pymysql.cursors - from pymysql.constants import CLIENT + from pymysql.constants import CLIENT, ER except Exception: # For local testing from top-level directory, without installing sys.path.append("../pymysql") from pymysql.tests import base import pymysql.cursors - from pymysql.constants import CLIENT + from pymysql.constants import CLIENT, ER class TestSSCursor(base.PyMySQLTestCase): @@ -122,6 +122,35 @@ def test_SSCursor(self): cursor.execute("DROP TABLE IF EXISTS tz_data") cursor.close() + def test_warnings(self): + con = self.connect() + cur = con.cursor(pymysql.cursors.SSCursor) + cur.execute("DROP TABLE IF EXISTS `no_exists_table`") + self.assertEqual(cur.warning_count, 1) + + cur.execute("SHOW WARNINGS") + w = cur.fetchone() + self.assertEqual(w[1], ER.BAD_TABLE_ERROR) + self.assertIn( + "no_exists_table", + w[2], + ) + + # ensure unbuffered result is finished + self.assertIsNone(cur.fetchone()) + + cur.execute("SELECT 1") + self.assertEqual(cur.fetchone(), (1,)) + self.assertIsNone(cur.fetchone()) + + self.assertEqual(cur.warning_count, 0) + + cur.execute("SELECT CAST('abc' AS SIGNED)") + # this ensures fully retrieving the unbuffered result + rows = cur.fetchmany(2) + self.assertEqual(len(rows), 1) + self.assertEqual(cur.warning_count, 1) + __all__ = ["TestSSCursor"] diff --git a/pymysql/tests/test_cursor.py b/pymysql/tests/test_cursor.py index 783caf88..63ecce02 100644 --- a/pymysql/tests/test_cursor.py +++ b/pymysql/tests/test_cursor.py @@ -1,5 +1,4 @@ -import warnings - +from pymysql.constants import ER from pymysql.tests import base import pymysql.cursors @@ -129,3 +128,20 @@ def test_executemany(self): ) finally: cursor.execute("DROP TABLE IF EXISTS percent_test") + + def test_warnings(self): + con = self.connect() + cur = con.cursor() + cur.execute("DROP TABLE IF EXISTS `no_exists_table`") + self.assertEqual(cur.warning_count, 1) + + cur.execute("SHOW WARNINGS") + w = cur.fetchone() + self.assertEqual(w[1], ER.BAD_TABLE_ERROR) + self.assertIn( + "no_exists_table", + w[2], + ) + + cur.execute("SELECT 1") + self.assertEqual(cur.warning_count, 0) diff --git a/pymysql/tests/test_load_local.py b/pymysql/tests/test_load_local.py index b1b8128e..194c5be9 100644 --- a/pymysql/tests/test_load_local.py +++ b/pymysql/tests/test_load_local.py @@ -1,4 +1,5 @@ from pymysql import cursors, OperationalError, Warning +from pymysql.constants import ER from pymysql.tests import base import os @@ -63,6 +64,37 @@ def test_unbuffered_load_file(self): c = conn.cursor() c.execute("DROP TABLE test_load_local") + def test_load_warnings(self): + """Test load local infile produces the appropriate warnings""" + conn = self.connect() + c = conn.cursor() + c.execute("CREATE TABLE test_load_local (a INTEGER, b INTEGER)") + filename = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "data", + "load_local_warn_data.txt", + ) + try: + c.execute( + ( + "LOAD DATA LOCAL INFILE '{0}' INTO TABLE " + + "test_load_local FIELDS TERMINATED BY ','" + ).format(filename) + ) + self.assertEqual(1, c.warning_count) + + c.execute("SHOW WARNINGS") + w = c.fetchone() + + self.assertEqual(ER.TRUNCATED_WRONG_VALUE_FOR_FIELD, w[1]) + self.assertIn( + "incorrect integer value", + w[2].lower(), + ) + finally: + c.execute("DROP TABLE test_load_local") + c.close() + if __name__ == "__main__": import unittest From ea79b3216e948ca1095bc7802e798bc3eb9dd599 Mon Sep 17 00:00:00 2001 From: Richard Schwab Date: Tue, 23 May 2023 14:18:40 +0200 Subject: [PATCH 08/80] Add constants and tests related to query timeouts (#1033) --- pymysql/constants/ER.py | 3 + pymysql/tests/base.py | 8 +++ pymysql/tests/test_SSCursor.py | 101 +++++++++++++++++++++++++++++---- pymysql/tests/test_cursor.py | 67 ++++++++++++++++++++++ 4 files changed, 168 insertions(+), 11 deletions(-) diff --git a/pymysql/constants/ER.py b/pymysql/constants/ER.py index ddcc4e90..98729d12 100644 --- a/pymysql/constants/ER.py +++ b/pymysql/constants/ER.py @@ -470,5 +470,8 @@ WRONG_STRING_LENGTH = 1468 ERROR_LAST = 1468 +# MariaDB only +STATEMENT_TIMEOUT = 1969 +QUERY_TIMEOUT = 3024 # https://github.com/PyMySQL/PyMySQL/issues/607 CONSTRAINT_FAILED = 4025 diff --git a/pymysql/tests/base.py b/pymysql/tests/base.py index a87307a5..ff33bc4e 100644 --- a/pymysql/tests/base.py +++ b/pymysql/tests/base.py @@ -49,6 +49,14 @@ def mysql_server_is(self, conn, version_tuple): ) return server_version_tuple >= version_tuple + def get_mysql_vendor(self, conn): + server_version = conn.get_server_info() + + if "MariaDB" in server_version: + return "mariadb" + + return "mysql" + _connections = None @property diff --git a/pymysql/tests/test_SSCursor.py b/pymysql/tests/test_SSCursor.py index d19d3e5d..9cb5bafe 100644 --- a/pymysql/tests/test_SSCursor.py +++ b/pymysql/tests/test_SSCursor.py @@ -1,15 +1,8 @@ -import sys +import pytest -try: - from pymysql.tests import base - import pymysql.cursors - from pymysql.constants import CLIENT, ER -except Exception: - # For local testing from top-level directory, without installing - sys.path.append("../pymysql") - from pymysql.tests import base - import pymysql.cursors - from pymysql.constants import CLIENT, ER +from pymysql.tests import base +import pymysql.cursors +from pymysql.constants import CLIENT, ER class TestSSCursor(base.PyMySQLTestCase): @@ -122,6 +115,92 @@ def test_SSCursor(self): cursor.execute("DROP TABLE IF EXISTS tz_data") cursor.close() + def test_execution_time_limit(self): + # this method is similarly implemented in test_cursor + + conn = self.connect() + + # table creation and filling is SSCursor only as it's not provided by self.setUp() + self.safe_create_table( + conn, + "test", + "create table test (data varchar(10))", + ) + with conn.cursor() as cur: + cur.execute( + "insert into test (data) values " + "('row1'), ('row2'), ('row3'), ('row4'), ('row5')" + ) + conn.commit() + + db_type = self.get_mysql_vendor(conn) + + with conn.cursor(pymysql.cursors.SSCursor) as cur: + # MySQL MAX_EXECUTION_TIME takes ms + # MariaDB max_statement_time takes seconds as int/float, introduced in 10.1 + + # this will sleep 0.01 seconds per row + if db_type == "mysql": + sql = ( + "SELECT /*+ MAX_EXECUTION_TIME(2000) */ data, sleep(0.01) FROM test" + ) + else: + sql = "SET STATEMENT max_statement_time=2 FOR SELECT data, sleep(0.01) FROM test" + + cur.execute(sql) + # unlike Cursor, SSCursor returns a list of tuples here + self.assertEqual( + cur.fetchall(), + [ + ("row1", 0), + ("row2", 0), + ("row3", 0), + ("row4", 0), + ("row5", 0), + ], + ) + + if db_type == "mysql": + sql = ( + "SELECT /*+ MAX_EXECUTION_TIME(2000) */ data, sleep(0.01) FROM test" + ) + else: + sql = "SET STATEMENT max_statement_time=2 FOR SELECT data, sleep(0.01) FROM test" + cur.execute(sql) + self.assertEqual(cur.fetchone(), ("row1", 0)) + + # this discards the previous unfinished query and raises an + # incomplete unbuffered query warning + with pytest.warns(UserWarning): + cur.execute("SELECT 1") + self.assertEqual(cur.fetchone(), (1,)) + + # SSCursor will not read the EOF packet until we try to read + # another row. Skipping this will raise an incomplete unbuffered + # query warning in the next cur.execute(). + self.assertEqual(cur.fetchone(), None) + + if db_type == "mysql": + sql = "SELECT /*+ MAX_EXECUTION_TIME(1) */ data, sleep(1) FROM test" + else: + sql = "SET STATEMENT max_statement_time=0.001 FOR SELECT data, sleep(1) FROM test" + with pytest.raises(pymysql.err.OperationalError) as cm: + # in an unbuffered cursor the OperationalError may not show up + # until fetching the entire result + cur.execute(sql) + cur.fetchall() + + if db_type == "mysql": + # this constant was only introduced in MySQL 5.7, not sure + # what was returned before, may have been ER_QUERY_INTERRUPTED + self.assertEqual(cm.value.args[0], ER.QUERY_TIMEOUT) + else: + self.assertEqual(cm.value.args[0], ER.STATEMENT_TIMEOUT) + + # connection should still be fine at this point + cur.execute("SELECT 1") + self.assertEqual(cur.fetchone(), (1,)) + def test_warnings(self): con = self.connect() cur = con.cursor(pymysql.cursors.SSCursor) diff --git a/pymysql/tests/test_cursor.py b/pymysql/tests/test_cursor.py index 63ecce02..66d968df 100644 --- a/pymysql/tests/test_cursor.py +++ b/pymysql/tests/test_cursor.py @@ -2,6 +2,8 @@ from pymysql.tests import base import pymysql.cursors +import pytest + class CursorTest(base.PyMySQLTestCase): def setUp(self): @@ -18,6 +20,7 @@ def setUp(self): "insert into test (data) values " "('row1'), ('row2'), ('row3'), ('row4'), ('row5')" ) + conn.commit() cursor.close() self.test_connection = pymysql.connect(**self.databases[0]) self.addCleanup(self.test_connection.close) @@ -129,6 +132,70 @@ def test_executemany(self): finally: cursor.execute("DROP TABLE IF EXISTS percent_test") + def test_execution_time_limit(self): + # this method is similarly implemented in test_SScursor + + conn = self.test_connection + db_type = self.get_mysql_vendor(conn) + + with conn.cursor(pymysql.cursors.Cursor) as cur: + # MySQL MAX_EXECUTION_TIME takes ms + # MariaDB max_statement_time takes seconds as int/float, introduced in 10.1 + + # this will sleep 0.01 seconds per row + if db_type == "mysql": + sql = ( + "SELECT /*+ MAX_EXECUTION_TIME(2000) */ data, sleep(0.01) FROM test" + ) + else: + sql = "SET STATEMENT max_statement_time=2 FOR SELECT data, sleep(0.01) FROM test" + + cur.execute(sql) + # unlike SSCursor, Cursor returns a tuple of tuples here + self.assertEqual( + cur.fetchall(), + ( + ("row1", 0), + ("row2", 0), + ("row3", 0), + ("row4", 0), + ("row5", 0), + ), + ) + + if db_type == "mysql": + sql = ( + "SELECT /*+ MAX_EXECUTION_TIME(2000) */ data, sleep(0.01) FROM test" + ) + else: + sql = "SET STATEMENT max_statement_time=2 FOR SELECT data, sleep(0.01) FROM test" + cur.execute(sql) + self.assertEqual(cur.fetchone(), ("row1", 0)) + + # this discards the previous unfinished query + cur.execute("SELECT 1") + self.assertEqual(cur.fetchone(), (1,)) + + if db_type == "mysql": + sql = "SELECT /*+ MAX_EXECUTION_TIME(1) */ data, sleep(1) FROM test" + else: + sql = "SET STATEMENT max_statement_time=0.001 FOR SELECT data, sleep(1) FROM test" + with pytest.raises(pymysql.err.OperationalError) as cm: + # in a buffered cursor this should reliably raise an + # OperationalError + cur.execute(sql) + + if db_type == "mysql": + # this constant was only introduced in MySQL 5.7, not sure + # what was returned before, may have been ER_QUERY_INTERRUPTED + self.assertEqual(cm.value.args[0], ER.QUERY_TIMEOUT) + else: + self.assertEqual(cm.value.args[0], ER.STATEMENT_TIMEOUT) + + # connection should still be fine at this point + cur.execute("SELECT 1") + self.assertEqual(cur.fetchone(), (1,)) + def test_warnings(self): con = self.connect() cur = con.cursor() From 2ee4f706d34412a6d39417b92360bfa13ddc4e14 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 23 May 2023 21:46:00 +0900 Subject: [PATCH 09/80] Fix wrong merge --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76fdb6a7..ce74e84b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,6 @@ Release date: TBD * Dropped support of end of life MySQL version 5.6 * Dropped support of end of life MariaDB versions below 10.3 * Dropped support of end of life Python version 3.6 -* Exposed `Cursor.warning_count` to check for warnings without additional query (#1056) ## v1.0.2 From 3cd76d7256416e3aa9575b3b9823c9491f92369c Mon Sep 17 00:00:00 2001 From: Richard Schwab Date: Tue, 23 May 2023 14:47:38 +0200 Subject: [PATCH 10/80] Fix SSCursor raising query timeout error on wrong query on MySQL DB (#1035) Fixes https://github.com/PyMySQL/PyMySQL/issues/1032#issuecomment-1030764742 --- CHANGELOG.md | 2 ++ pymysql/connections.py | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce74e84b..6dc75225 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,10 @@ Release date: TBD +* Fixed SSCursor raising OperationalError for query timeouts on wrong statement (#1032) * Exposed `Cursor.warning_count` to check for warnings without additional query (#1056) + ## v1.0.3 Release date: TBD diff --git a/pymysql/connections.py b/pymysql/connections.py index 3265d32e..f82b1951 100644 --- a/pymysql/connections.py +++ b/pymysql/connections.py @@ -1262,7 +1262,20 @@ def _finish_unbuffered_query(self): # in fact, no way to stop MySQL from sending all the data after # executing a query, so we just spin, and wait for an EOF packet. while self.unbuffered_active: - packet = self.connection._read_packet() + try: + packet = self.connection._read_packet() + except err.OperationalError as e: + if e.args[0] in ( + ER.QUERY_TIMEOUT, + ER.STATEMENT_TIMEOUT, + ): + # if the query timed out we can simply ignore this error + self.unbuffered_active = False + self.connection = None + return + + raise + if self._check_packet_is_eof(packet): self.unbuffered_active = False self.connection = None # release reference to kill cyclic reference. From a6f53dbffa5ee6986b0c48c32e43bd071a04217d Mon Sep 17 00:00:00 2001 From: Gonzalo Sanchez Date: Tue, 23 May 2023 12:43:50 -0300 Subject: [PATCH 11/80] Make Cursor an iterator (#995) Fix #992 Co-authored-by: Gonzalo Sanchez Co-authored-by: Inada Naoki --- pymysql/cursors.py | 11 +++++++---- pymysql/tests/test_cursor.py | 8 ++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/pymysql/cursors.py b/pymysql/cursors.py index e57fba76..d8a93c78 100644 --- a/pymysql/cursors.py +++ b/pymysql/cursors.py @@ -342,7 +342,13 @@ def _do_get_result(self): self._rows = result.rows def __iter__(self): - return iter(self.fetchone, None) + return self + + def __next__(self): + row = self.fetchone() + if row is None: + raise StopIteration + return row Warning = err.Warning Error = err.Error @@ -459,9 +465,6 @@ def fetchall_unbuffered(self): """ return iter(self.fetchone, None) - def __iter__(self): - return self.fetchall_unbuffered() - def fetchmany(self, size=None): """Fetch many.""" self._check_executed() diff --git a/pymysql/tests/test_cursor.py b/pymysql/tests/test_cursor.py index 66d968df..16d297f6 100644 --- a/pymysql/tests/test_cursor.py +++ b/pymysql/tests/test_cursor.py @@ -25,6 +25,14 @@ def setUp(self): self.test_connection = pymysql.connect(**self.databases[0]) self.addCleanup(self.test_connection.close) + def test_cursor_is_iterator(self): + """Test that the cursor is an iterator""" + conn = self.test_connection + cursor = conn.cursor() + cursor.execute("select * from test") + self.assertEqual(cursor.__iter__(), cursor) + self.assertEqual(cursor.__next__(), ("row1",)) + def test_cleanup_rows_unbuffered(self): conn = self.test_connection cursor = conn.cursor(pymysql.cursors.SSCursor) From 4072c7fff9871f6eb811b9b4442bbb5411b6d01b Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 24 May 2023 01:17:31 +0900 Subject: [PATCH 12/80] ci: Update CodeQL workflow (#1110) --- .github/workflows/codeql-analysis.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d559b1cd..a4c434c5 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,21 +27,16 @@ jobs: strategy: fail-fast: false - matrix: - language: [ 'python' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: - languages: ${{ matrix.language }} + 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. From 2fe0b1293d1a24140f6d35f5ff37d7b5a46a28e1 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 24 May 2023 14:17:19 +0900 Subject: [PATCH 13/80] Use Ruff instead of flake8 (#1112) --- .flake8 | 4 - .github/workflows/lint.yaml | 13 +--- pymysql/_auth.py | 3 +- pymysql/connections.py | 15 ++-- pymysql/converters.py | 5 +- pymysql/tests/__init__.py | 19 ----- pymysql/tests/base.py | 1 - pymysql/tests/test_basic.py | 28 +++++-- pymysql/tests/test_connection.py | 75 +++++++------------ pymysql/tests/test_cursor.py | 3 +- pymysql/tests/test_issues.py | 2 - pymysql/tests/test_load_local.py | 8 +- .../tests/thirdparty/test_MySQLdb/__init__.py | 2 - .../thirdparty/test_MySQLdb/capabilities.py | 1 - .../tests/thirdparty/test_MySQLdb/dbapi20.py | 20 +++-- .../test_MySQLdb/test_MySQLdb_capabilities.py | 1 - .../test_MySQLdb/test_MySQLdb_dbapi20.py | 4 - .../test_MySQLdb/test_MySQLdb_nonstandard.py | 1 - pyproject.toml | 6 ++ 19 files changed, 89 insertions(+), 122 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 3f1c38a3..00000000 --- a/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -exclude = tests,build,.venv,docs -ignore = E203,W503,E722 -max_line_length=129 diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 9d9eafb0..77edb0c3 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -2,6 +2,7 @@ name: Lint on: push: + branches: ["main"] paths: - '**.py' pull_request: @@ -13,16 +14,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: 3.x + - uses: psf/black@stable with: options: "--check --verbose" src: "." - - name: Setup flake8 annotations - uses: rbialon/flake8-annotations@v1 - - name: flake8 - run: | - pip install flake8 - flake8 pymysql + + - uses: chartboost/ruff-action@v1 diff --git a/pymysql/_auth.py b/pymysql/_auth.py index f6c9eb96..99987b77 100644 --- a/pymysql/_auth.py +++ b/pymysql/_auth.py @@ -141,7 +141,8 @@ def sha2_rsa_encrypt(password, salt, public_key): """ if not _have_cryptography: raise RuntimeError( - "'cryptography' package is required for sha256_password or caching_sha2_password auth methods" + "'cryptography' package is required for sha256_password or" + + " caching_sha2_password auth methods" ) message = _xor_password(password + b"\0", salt) rsa_key = serialization.load_pem_public_key(public_key, default_backend()) diff --git a/pymysql/connections.py b/pymysql/connections.py index f82b1951..7bbc089f 100644 --- a/pymysql/connections.py +++ b/pymysql/connections.py @@ -108,8 +108,10 @@ class Connection: the interface from which to connect to the host. Argument can be a hostname or an IP address. :param unix_socket: Use a unix socket rather than TCP/IP. - :param read_timeout: The timeout for reading from the connection in seconds (default: None - no timeout) - :param write_timeout: The timeout for writing to the connection in seconds (default: None - no timeout) + :param read_timeout: The timeout for reading from the connection in seconds. + (default: None - no timeout) + :param write_timeout: The timeout for writing to the connection in seconds. + (default: None - no timeout) :param charset: Charset to use. :param sql_mode: Default SQL_MODE to use. :param read_default_file: @@ -130,7 +132,8 @@ class Connection: :param ssl_ca: Path to the file that contains a PEM-formatted CA certificate. :param ssl_cert: Path to the file that contains a PEM-formatted client certificate. :param ssl_disabled: A boolean value that disables usage of TLS. - :param ssl_key: Path to the file that contains a PEM-formatted private key for the client certificate. + :param ssl_key: Path to the file that contains a PEM-formatted private key for + the client certificate. :param ssl_verify_cert: Set to true to check the server certificate's validity. :param ssl_verify_identity: Set to true to check the server's identity. :param read_default_group: Group to read from in the configuration file. @@ -533,7 +536,8 @@ def cursor(self, cursor=None): Create a new cursor to execute queries with. :param cursor: The type of cursor to create. None means use Cursor. - :type cursor: :py:class:`Cursor`, :py:class:`SSCursor`, :py:class:`DictCursor`, or :py:class:`SSDictCursor`. + :type cursor: :py:class:`Cursor`, :py:class:`SSCursor`, :py:class:`DictCursor`, + or :py:class:`SSDictCursor`. """ if cursor: return cursor(self) @@ -1228,7 +1232,8 @@ def _check_packet_is_eof(self, packet): # TODO: Support CLIENT.DEPRECATE_EOF # 1) Add DEPRECATE_EOF to CAPABILITIES # 2) Mask CAPABILITIES with server_capabilities - # 3) if server_capabilities & CLIENT.DEPRECATE_EOF: use OKPacketWrapper instead of EOFPacketWrapper + # 3) if server_capabilities & CLIENT.DEPRECATE_EOF: + # use OKPacketWrapper instead of EOFPacketWrapper wp = EOFPacketWrapper(packet) self.warning_count = wp.warning_count self.has_next = wp.has_next diff --git a/pymysql/converters.py b/pymysql/converters.py index 2acc3e58..1adac752 100644 --- a/pymysql/converters.py +++ b/pymysql/converters.py @@ -120,7 +120,10 @@ def escape_time(obj, mapping=None): def escape_datetime(obj, mapping=None): if obj.microsecond: - fmt = "'{0.year:04}-{0.month:02}-{0.day:02} {0.hour:02}:{0.minute:02}:{0.second:02}.{0.microsecond:06}'" + fmt = ( + "'{0.year:04}-{0.month:02}-{0.day:02}" + + " {0.hour:02}:{0.minute:02}:{0.second:02}.{0.microsecond:06}'" + ) else: fmt = "'{0.year:04}-{0.month:02}-{0.day:02} {0.hour:02}:{0.minute:02}:{0.second:02}'" return fmt.format(obj) diff --git a/pymysql/tests/__init__.py b/pymysql/tests/__init__.py index fe3b1d0f..e69de29b 100644 --- a/pymysql/tests/__init__.py +++ b/pymysql/tests/__init__.py @@ -1,19 +0,0 @@ -# Sorted by alphabetical order -from pymysql.tests.test_DictCursor import * -from pymysql.tests.test_SSCursor import * -from pymysql.tests.test_basic import * -from pymysql.tests.test_connection import * -from pymysql.tests.test_converters import * -from pymysql.tests.test_cursor import * -from pymysql.tests.test_err import * -from pymysql.tests.test_issues import * -from pymysql.tests.test_load_local import * -from pymysql.tests.test_nextset import * -from pymysql.tests.test_optionfile import * - -from pymysql.tests.thirdparty import * - -if __name__ == "__main__": - import unittest - - unittest.main() diff --git a/pymysql/tests/base.py b/pymysql/tests/base.py index ff33bc4e..b5094563 100644 --- a/pymysql/tests/base.py +++ b/pymysql/tests/base.py @@ -1,4 +1,3 @@ -import gc import json import os import re diff --git a/pymysql/tests/test_basic.py b/pymysql/tests/test_basic.py index 8af07da0..ecf043f6 100644 --- a/pymysql/tests/test_basic.py +++ b/pymysql/tests/test_basic.py @@ -6,7 +6,6 @@ import pymysql.cursors from pymysql.tests import base -from pymysql.err import ProgrammingError __all__ = ["TestConversion", "TestCursor", "TestBulkInserts"] @@ -18,7 +17,22 @@ def test_datatypes(self): conn = self.connect() c = conn.cursor() c.execute( - "create table test_datatypes (b bit, i int, l bigint, f real, s varchar(32), u varchar(32), bb blob, d date, dt datetime, ts timestamp, td time, t time, st datetime)" + """ +create table test_datatypes ( + b bit, + i int, + l bigint, + f real, + s varchar(32), + u varchar(32), + bb blob, + d date, + dt datetime, + ts timestamp, + td time, + t time, + st datetime) +""" ) try: # insert values @@ -38,7 +52,8 @@ def test_datatypes(self): time.localtime(), ) c.execute( - "insert into test_datatypes (b,i,l,f,s,u,bb,d,dt,td,t,st) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", + "insert into test_datatypes (b,i,l,f,s,u,bb,d,dt,td,t,st) values" + " (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", v, ) c.execute("select b,i,l,f,s,u,bb,d,dt,td,t,st from test_datatypes") @@ -54,7 +69,8 @@ def test_datatypes(self): # check nulls c.execute( - "insert into test_datatypes (b,i,l,f,s,u,bb,d,dt,td,t,st) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", + "insert into test_datatypes (b,i,l,f,s,u,bb,d,dt,td,t,st)" + " values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", [None] * 12, ) c.execute("select b,i,l,f,s,u,bb,d,dt,td,t,st from test_datatypes") @@ -156,7 +172,8 @@ def test_timedelta(self): conn = self.connect() c = conn.cursor() c.execute( - "select time('12:30'), time('23:12:59'), time('23:12:59.05100'), time('-12:30'), time('-23:12:59'), time('-23:12:59.05100'), time('-00:30')" + "select time('12:30'), time('23:12:59'), time('23:12:59.05100')," + + " time('-12:30'), time('-23:12:59'), time('-23:12:59.05100'), time('-00:30')" ) self.assertEqual( ( @@ -317,7 +334,6 @@ class TestBulkInserts(base.PyMySQLTestCase): def setUp(self): super(TestBulkInserts, self).setUp() self.conn = conn = self.connect() - c = conn.cursor(self.cursor_type) # create a table and some data to query self.safe_create_table( diff --git a/pymysql/tests/test_connection.py b/pymysql/tests/test_connection.py index d6fb5e52..bbaf3dec 100644 --- a/pymysql/tests/test_connection.py +++ b/pymysql/tests/test_connection.py @@ -1,6 +1,5 @@ import datetime import ssl -import sys import pytest import time from unittest import mock @@ -145,8 +144,8 @@ def realtestSocketAuth(self): TestAuthentication.osuser + "@localhost", self.databases[0]["database"], self.socket_plugin_name, - ) as u: - c = pymysql.connect(user=TestAuthentication.osuser, **self.db) + ): + pymysql.connect(user=TestAuthentication.osuser, **self.db) class Dialog: fail = False @@ -168,7 +167,7 @@ def __init__(self, con): def authenticate(self, pkt): while True: flag = pkt.read_uint8() - echo = (flag & 0x06) == 0x02 + # echo = (flag & 0x06) == 0x02 last = (flag & 0x01) == 0x01 prompt = pkt.read_all() @@ -220,7 +219,7 @@ def realTestDialogAuthTwoQuestions(self): self.databases[0]["database"], "two_questions", "notverysecret", - ) as u: + ): with self.assertRaises(pymysql.err.OperationalError): pymysql.connect(user="pymysql_2q", **self.db) pymysql.connect( @@ -262,7 +261,7 @@ def realTestDialogAuthThreeAttempts(self): self.databases[0]["database"], "three_attempts", "stillnotverysecret", - ) as u: + ): pymysql.connect( user="pymysql_3a", auth_plugin_map={b"dialog": TestAuthentication.Dialog}, @@ -357,9 +356,9 @@ def realTestPamAuth(self): self.databases[0]["database"], "pam", os.environ.get("PAMSERVICE"), - ) as u: + ): try: - c = pymysql.connect(user=TestAuthentication.osuser, **db) + pymysql.connect(user=TestAuthentication.osuser, **db) db["password"] = "very bad guess at password" with self.assertRaises(pymysql.err.OperationalError): pymysql.connect( @@ -371,7 +370,8 @@ def realTestPamAuth(self): ) except pymysql.OperationalError as e: self.assertEqual(1045, e.args[0]) - # we had 'bad guess at password' work with pam. Well at least we get a permission denied here + # we had 'bad guess at password' work with pam. Well at least we get + # a permission denied here with self.assertRaises(pymysql.err.OperationalError): pymysql.connect( user=TestAuthentication.osuser, @@ -397,12 +397,13 @@ def testAuthSHA256(self): "pymysql_sha256@localhost", self.databases[0]["database"], "sha256_password", - ) as u: + ): c.execute("SET PASSWORD FOR 'pymysql_sha256'@'localhost' ='Sh@256Pa33'") c.execute("FLUSH PRIVILEGES") db = self.db.copy() db["password"] = "Sh@256Pa33" - # Although SHA256 is supported, need the configuration of public key of the mysql server. Currently will get error by this test. + # Although SHA256 is supported, need the configuration of public key of + # the mysql server. Currently will get error by this test. with self.assertRaises(pymysql.err.OperationalError): pymysql.connect(user="pymysql_sha256", **db) @@ -423,7 +424,7 @@ def testAuthEd25519(self): self.databases[0]["database"], "ed25519", empty_pass, - ) as u: + ): pymysql.connect(user="pymysql_ed25519", password="", **db) with TempUser( @@ -432,7 +433,7 @@ def testAuthEd25519(self): self.databases[0]["database"], "ed25519", non_empty_pass, - ) as u: + ): pymysql.connect(user="pymysql_ed25519", password="ed25519_password", **db) @@ -441,7 +442,7 @@ def test_utf8mb4(self): """This test requires MySQL >= 5.5""" arg = self.databases[0].copy() arg["charset"] = "utf8mb4" - conn = pymysql.connect(**arg) + pymysql.connect(**arg) def test_largedata(self): """Large query and response (>=16MB)""" @@ -544,9 +545,7 @@ def test_defer_connect(self): def test_ssl_connect(self): dummy_ssl_context = mock.Mock(options=0) - with mock.patch( - "pymysql.connections.Connection.connect" - ) as connect, mock.patch( + with mock.patch("pymysql.connections.Connection.connect"), mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), ) as create_default_context: @@ -565,9 +564,7 @@ def test_ssl_connect(self): dummy_ssl_context.set_ciphers.assert_called_with("cipher") dummy_ssl_context = mock.Mock(options=0) - with mock.patch( - "pymysql.connections.Connection.connect" - ) as connect, mock.patch( + with mock.patch("pymysql.connections.Connection.connect"), mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), ) as create_default_context: @@ -585,9 +582,7 @@ def test_ssl_connect(self): dummy_ssl_context.set_ciphers.assert_not_called dummy_ssl_context = mock.Mock(options=0) - with mock.patch( - "pymysql.connections.Connection.connect" - ) as connect, mock.patch( + with mock.patch("pymysql.connections.Connection.connect"), mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), ) as create_default_context: @@ -601,9 +596,7 @@ def test_ssl_connect(self): dummy_ssl_context.set_ciphers.assert_not_called dummy_ssl_context = mock.Mock(options=0) - with mock.patch( - "pymysql.connections.Connection.connect" - ) as connect, mock.patch( + with mock.patch("pymysql.connections.Connection.connect"), mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), ) as create_default_context: @@ -620,9 +613,7 @@ def test_ssl_connect(self): for ssl_verify_cert in (True, "1", "yes", "true"): dummy_ssl_context = mock.Mock(options=0) - with mock.patch( - "pymysql.connections.Connection.connect" - ) as connect, mock.patch( + with mock.patch("pymysql.connections.Connection.connect"), mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), ) as create_default_context: @@ -641,9 +632,7 @@ def test_ssl_connect(self): for ssl_verify_cert in (None, False, "0", "no", "false"): dummy_ssl_context = mock.Mock(options=0) - with mock.patch( - "pymysql.connections.Connection.connect" - ) as connect, mock.patch( + with mock.patch("pymysql.connections.Connection.connect"), mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), ) as create_default_context: @@ -663,9 +652,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) - with mock.patch( - "pymysql.connections.Connection.connect" - ) as connect, mock.patch( + with mock.patch("pymysql.connections.Connection.connect"), mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), ) as create_default_context: @@ -686,9 +673,7 @@ def test_ssl_connect(self): dummy_ssl_context.set_ciphers.assert_not_called dummy_ssl_context = mock.Mock(options=0) - with mock.patch( - "pymysql.connections.Connection.connect" - ) as connect, mock.patch( + with mock.patch("pymysql.connections.Connection.connect"), mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), ) as create_default_context: @@ -705,9 +690,7 @@ def test_ssl_connect(self): dummy_ssl_context.set_ciphers.assert_not_called dummy_ssl_context = mock.Mock(options=0) - with mock.patch( - "pymysql.connections.Connection.connect" - ) as connect, mock.patch( + with mock.patch("pymysql.connections.Connection.connect"), mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), ) as create_default_context: @@ -722,9 +705,7 @@ def test_ssl_connect(self): assert not create_default_context.called dummy_ssl_context = mock.Mock(options=0) - with mock.patch( - "pymysql.connections.Connection.connect" - ) as connect, mock.patch( + with mock.patch("pymysql.connections.Connection.connect"), mock.patch( "pymysql.connections.ssl.create_default_context", new=mock.Mock(return_value=dummy_ssl_context), ) as create_default_context: @@ -762,21 +743,18 @@ def test_escape_string(self): def test_escape_builtin_encoders(self): con = self.connect() - cur = con.cursor() val = datetime.datetime(2012, 3, 4, 5, 6) self.assertEqual(con.escape(val, con.encoders), "'2012-03-04 05:06:00'") def test_escape_custom_object(self): con = self.connect() - cur = con.cursor() mapping = {Foo: escape_foo} self.assertEqual(con.escape(Foo(), mapping), "bar") def test_escape_fallback_encoder(self): con = self.connect() - cur = con.cursor() class Custom(str): pass @@ -786,13 +764,11 @@ class Custom(str): def test_escape_no_default(self): con = self.connect() - cur = con.cursor() self.assertRaises(TypeError, con.escape, 42, {}) def test_escape_dict_value(self): con = self.connect() - cur = con.cursor() mapping = con.encoders.copy() mapping[Foo] = escape_foo @@ -800,7 +776,6 @@ def test_escape_dict_value(self): def test_escape_list_item(self): con = self.connect() - cur = con.cursor() mapping = con.encoders.copy() mapping[Foo] = escape_foo diff --git a/pymysql/tests/test_cursor.py b/pymysql/tests/test_cursor.py index 16d297f6..6666ab88 100644 --- a/pymysql/tests/test_cursor.py +++ b/pymysql/tests/test_cursor.py @@ -105,7 +105,8 @@ def test_executemany(self): ) assert m is not None - # cursor._executed must bee "insert into test (data) values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)" + # cursor._executed must bee "insert into test (data) + # values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)" # list args data = range(10) cursor.executemany("insert into test (data) values (%s)", data) diff --git a/pymysql/tests/test_issues.py b/pymysql/tests/test_issues.py index 733d56a1..7f361c94 100644 --- a/pymysql/tests/test_issues.py +++ b/pymysql/tests/test_issues.py @@ -1,12 +1,10 @@ import datetime import time import warnings -import sys import pytest import pymysql -from pymysql import cursors from pymysql.tests import base __all__ = ["TestOldIssues", "TestNewIssues", "TestGitHubIssues"] diff --git a/pymysql/tests/test_load_local.py b/pymysql/tests/test_load_local.py index 194c5be9..50922142 100644 --- a/pymysql/tests/test_load_local.py +++ b/pymysql/tests/test_load_local.py @@ -1,4 +1,4 @@ -from pymysql import cursors, OperationalError, Warning +from pymysql import cursors, OperationalError from pymysql.constants import ER from pymysql.tests import base @@ -36,7 +36,8 @@ def test_load_file(self): ) try: c.execute( - f"LOAD DATA LOCAL INFILE '{filename}' INTO TABLE test_load_local FIELDS TERMINATED BY ','" + f"LOAD DATA LOCAL INFILE '{filename}' INTO TABLE test_load_local" + + " FIELDS TERMINATED BY ','" ) c.execute("SELECT COUNT(*) FROM test_load_local") self.assertEqual(22749, c.fetchone()[0]) @@ -53,7 +54,8 @@ def test_unbuffered_load_file(self): ) try: c.execute( - f"LOAD DATA LOCAL INFILE '{filename}' INTO TABLE test_load_local FIELDS TERMINATED BY ','" + f"LOAD DATA LOCAL INFILE '{filename}' INTO TABLE test_load_local" + + " FIELDS TERMINATED BY ','" ) c.execute("SELECT COUNT(*) FROM test_load_local") self.assertEqual(22749, c.fetchone()[0]) diff --git a/pymysql/tests/thirdparty/test_MySQLdb/__init__.py b/pymysql/tests/thirdparty/test_MySQLdb/__init__.py index 57c42ce7..501bfd2d 100644 --- a/pymysql/tests/thirdparty/test_MySQLdb/__init__.py +++ b/pymysql/tests/thirdparty/test_MySQLdb/__init__.py @@ -1,6 +1,4 @@ -from .test_MySQLdb_capabilities import test_MySQLdb as test_capabilities from .test_MySQLdb_nonstandard import * -from .test_MySQLdb_dbapi20 import test_MySQLdb as test_dbapi2 if __name__ == "__main__": import unittest diff --git a/pymysql/tests/thirdparty/test_MySQLdb/capabilities.py b/pymysql/tests/thirdparty/test_MySQLdb/capabilities.py index 0276a558..bb47cc5f 100644 --- a/pymysql/tests/thirdparty/test_MySQLdb/capabilities.py +++ b/pymysql/tests/thirdparty/test_MySQLdb/capabilities.py @@ -4,7 +4,6 @@ Adapted from a script by M-A Lemburg. """ -import sys from time import time import unittest diff --git a/pymysql/tests/thirdparty/test_MySQLdb/dbapi20.py b/pymysql/tests/thirdparty/test_MySQLdb/dbapi20.py index 30620ce4..83851295 100644 --- a/pymysql/tests/thirdparty/test_MySQLdb/dbapi20.py +++ b/pymysql/tests/thirdparty/test_MySQLdb/dbapi20.py @@ -225,7 +225,7 @@ def test_rollback(self): def test_cursor(self): con = self._connect() try: - cur = con.cursor() + con.cursor() finally: con.close() @@ -810,28 +810,26 @@ def test_None(self): con.close() def test_Date(self): - d1 = self.driver.Date(2002, 12, 25) - d2 = self.driver.DateFromTicks(time.mktime((2002, 12, 25, 0, 0, 0, 0, 0, 0))) + self.driver.Date(2002, 12, 25) + self.driver.DateFromTicks(time.mktime((2002, 12, 25, 0, 0, 0, 0, 0, 0))) # Can we assume this? API doesn't specify, but it seems implied # self.assertEqual(str(d1),str(d2)) def test_Time(self): - t1 = self.driver.Time(13, 45, 30) - t2 = self.driver.TimeFromTicks(time.mktime((2001, 1, 1, 13, 45, 30, 0, 0, 0))) + self.driver.Time(13, 45, 30) + self.driver.TimeFromTicks(time.mktime((2001, 1, 1, 13, 45, 30, 0, 0, 0))) # Can we assume this? API doesn't specify, but it seems implied # self.assertEqual(str(t1),str(t2)) def test_Timestamp(self): - t1 = self.driver.Timestamp(2002, 12, 25, 13, 45, 30) - t2 = self.driver.TimestampFromTicks( - time.mktime((2002, 12, 25, 13, 45, 30, 0, 0, 0)) - ) + self.driver.Timestamp(2002, 12, 25, 13, 45, 30) + self.driver.TimestampFromTicks(time.mktime((2002, 12, 25, 13, 45, 30, 0, 0, 0))) # Can we assume this? API doesn't specify, but it seems implied # self.assertEqual(str(t1),str(t2)) def test_Binary(self): - b = self.driver.Binary(b"Something") - b = self.driver.Binary(b"") + self.driver.Binary(b"Something") + self.driver.Binary(b"") def test_STRING(self): self.assertTrue(hasattr(self.driver, "STRING"), "module.STRING must be defined") diff --git a/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py b/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py index 11bfdbe2..6a2894a5 100644 --- a/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py +++ b/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py @@ -1,5 +1,4 @@ from . import capabilities -import unittest import pymysql from pymysql.tests import base import warnings diff --git a/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py b/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py index bc1e1b2e..c68289fe 100644 --- a/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py +++ b/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py @@ -2,8 +2,6 @@ import pymysql from pymysql.tests import base -import unittest - class test_MySQLdb(dbapi20.DatabaseAPI20Test): driver = pymysql @@ -181,8 +179,6 @@ def help_nextset_tearDown(self, cur): cur.execute("drop procedure deleteme") def test_nextset(self): - from warnings import warn - con = self._connect() try: cur = con.cursor() diff --git a/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py b/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py index b8d4bb1e..1545fbb5 100644 --- a/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py +++ b/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py @@ -1,4 +1,3 @@ -import sys import unittest import pymysql diff --git a/pyproject.toml b/pyproject.toml index a67031b3..48fe3660 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,3 +50,9 @@ exclude = ["tests*", "pymysql.tests*"] [tool.setuptools.dynamic] version = {attr = "pymysql.VERSION"} + +[tool.ruff] +line-length = 99 +exclude = [ + "pymysql/tests/thirdparty", +] From d02e090e7a4766584750720d058bcc8e46eec48f Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 24 May 2023 14:50:22 +0900 Subject: [PATCH 14/80] Use Codecov instead of coveralls. (#1113) --- .github/workflows/test.yaml | 31 +++---------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 993347f6..bea7747c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -44,6 +44,7 @@ jobs: - 3306:3306 env: MYSQL_ALLOW_EMPTY_PASSWORD: yes + MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: yes options: "--name=mysqld" volumes: - /run/mysqld:/run/mysqld @@ -96,32 +97,6 @@ jobs: docker cp mysqld:/var/lib/mysql/client-cert.pem "${HOME}" pytest -v --cov --cov-config .coveragerc tests/test_auth.py; - - name: Report coverage + - name: Upload coverage reports to Codecov if: github.repository == 'PyMySQL/PyMySQL' - run: coveralls --service=github - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COVERALLS_FLAG_NAME: ${{ matrix.py }}-${{ matrix.db }} - COVERALLS_PARALLEL: true - - coveralls: - if: github.repository == 'PyMySQL/PyMySQL' - name: Finish coveralls - runs-on: ubuntu-latest - needs: test - steps: - - name: requirements. - run: | - echo coveralls > requirements.txt - - - uses: actions/setup-python@v4 - with: - python-version: '3.x' - cache: 'pip' - - - name: Finished - run: | - pip install --upgrade coveralls - coveralls --finish --service=github - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: codecov/codecov-action@v3 From f5c0ac217b08e8a59f382bd252491de9f73d6f6a Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 24 May 2023 14:52:36 +0900 Subject: [PATCH 15/80] Update README codecov badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dec84080..6e6a6bf2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![Documentation Status](https://readthedocs.org/projects/pymysql/badge/?version=latest)](https://pymysql.readthedocs.io/) -[![image](https://coveralls.io/repos/PyMySQL/PyMySQL/badge.svg?branch=main&service=github)](https://coveralls.io/github/PyMySQL/PyMySQL?branch=main) +[![codecov](https://codecov.io/gh/PyMySQL/PyMySQL/branch/main/graph/badge.svg?token=ppEuaNXBW4)](https://codecov.io/gh/PyMySQL/PyMySQL) # PyMySQL From b39a43ade46eaacb081615a82bdc14ef62974ccf Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 24 May 2023 16:53:04 +0900 Subject: [PATCH 16/80] ci: Fix MySQL 8 build overwrite previous coverage --- .github/workflows/test.yaml | 2 +- pyproject.toml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index bea7747c..c3275cca 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -95,7 +95,7 @@ jobs: docker cp mysqld:/var/lib/mysql/server-cert.pem "${HOME}" docker cp mysqld:/var/lib/mysql/client-key.pem "${HOME}" docker cp mysqld:/var/lib/mysql/client-cert.pem "${HOME}" - pytest -v --cov --cov-config .coveragerc tests/test_auth.py; + pytest -v --cov-append --cov-config .coveragerc tests/test_auth.py; - name: Upload coverage reports to Codecov if: github.repository == 'PyMySQL/PyMySQL' diff --git a/pyproject.toml b/pyproject.toml index 48fe3660..18714779 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,3 +56,8 @@ line-length = 99 exclude = [ "pymysql/tests/thirdparty", ] + +[tool.pdm.dev-dependencies] +dev = [ + "pytest-cov>=4.0.0", +] From 92287000831deed476e6d4a8341c6210f984bda5 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 24 May 2023 22:24:24 +0900 Subject: [PATCH 17/80] optionfile: Replace `_` with `-` (#1114) Fix #1020 --- pymysql/optionfile.py | 3 +++ pymysql/tests/test_optionfile.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pymysql/optionfile.py b/pymysql/optionfile.py index 432621b7..c36f1625 100644 --- a/pymysql/optionfile.py +++ b/pymysql/optionfile.py @@ -13,6 +13,9 @@ def __remove_quotes(self, value): return value[1:-1] return value + def optionxform(self, key): + return key.lower().replace("_", "-") + def get(self, section, option): value = configparser.RawConfigParser.get(self, section, option) return self.__remove_quotes(value) diff --git a/pymysql/tests/test_optionfile.py b/pymysql/tests/test_optionfile.py index 39bd47c4..d13553dd 100644 --- a/pymysql/tests/test_optionfile.py +++ b/pymysql/tests/test_optionfile.py @@ -21,4 +21,4 @@ def test_string(self): parser.read_file(StringIO(_cfg_file)) self.assertEqual(parser.get("default", "string"), "foo") self.assertEqual(parser.get("default", "quoted"), "bar") - self.assertEqual(parser.get("default", "single_quoted"), "foobar") + self.assertEqual(parser.get("default", "single-quoted"), "foobar") From bfbc6a53db56d37993837ea59146995e7410b41b Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Thu, 25 May 2023 00:08:34 +0900 Subject: [PATCH 18/80] Cursor.fetchall() always return list. (#1115) Cursor.fetchmany() returns empty tuple when exhausted all rows. It is for Django compatibility. Fix #1042. --- pymysql/cursors.py | 8 +++++++- pymysql/tests/test_nextset.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pymysql/cursors.py b/pymysql/cursors.py index d8a93c78..e098e7de 100644 --- a/pymysql/cursors.py +++ b/pymysql/cursors.py @@ -282,6 +282,8 @@ def fetchmany(self, size=None): """Fetch several rows.""" self._check_executed() if self._rows is None: + # Django expects () for EOF. + # https://github.com/django/django/blob/0c1518ee429b01c145cf5b34eab01b0b92f8c246/django/db/backends/mysql/features.py#L8 return () end = self.rownumber + (size or self.arraysize) result = self._rows[self.rownumber : end] @@ -292,7 +294,7 @@ def fetchall(self): """Fetch all the rows.""" self._check_executed() if self._rows is None: - return () + return [] if self.rownumber: result = self._rows[self.rownumber :] else: @@ -479,6 +481,10 @@ def fetchmany(self, size=None): break rows.append(row) self.rownumber += 1 + if not rows: + # Django expects () for EOF. + # https://github.com/django/django/blob/0c1518ee429b01c145cf5b34eab01b0b92f8c246/django/db/backends/mysql/features.py#L8 + return () return rows def scroll(self, value, mode="relative"): diff --git a/pymysql/tests/test_nextset.py b/pymysql/tests/test_nextset.py index 28972325..4b6b2a77 100644 --- a/pymysql/tests/test_nextset.py +++ b/pymysql/tests/test_nextset.py @@ -38,7 +38,7 @@ def test_nextset_error(self): self.assertEqual([(i,)], list(cur)) with self.assertRaises(pymysql.ProgrammingError): cur.nextset() - self.assertEqual((), cur.fetchall()) + self.assertEqual([], cur.fetchall()) def test_ok_and_next(self): cur = self.connect(client_flag=CLIENT.MULTI_STATEMENTS).cursor() From bd3bd014999475242b5963b1af7990beaa6af6b5 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Thu, 25 May 2023 00:33:50 +0900 Subject: [PATCH 19/80] Fix LOAD DATA LOCAL INFILE write EOF packet on closed connection. (#1116) Fix #989 --- .gitignore | 1 + pymysql/connections.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 98f4d45c..09a5654f 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ /pymysql/tests/databases.json __pycache__ Pipfile.lock +pdm.lock diff --git a/pymysql/connections.py b/pymysql/connections.py index 7bbc089f..ef3342aa 100644 --- a/pymysql/connections.py +++ b/pymysql/connections.py @@ -1370,7 +1370,7 @@ def send_data(self): """Send data packets from the local file to the server""" if not self.connection._sock: raise err.InterfaceError(0, "") - conn = self.connection + conn: Connection = self.connection try: with open(self.filename, "rb") as open_file: @@ -1388,5 +1388,6 @@ def send_data(self): f"Can't find file '{self.filename}'", ) finally: - # send the empty packet to signify we are done sending data - conn.write_packet(b"") + if not conn._closed: + # send the empty packet to signify we are done sending data + conn.write_packet(b"") From 9a694a16a3a98ebf53cd14a1361db6c9faadba8f Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Thu, 25 May 2023 00:52:43 +0900 Subject: [PATCH 20/80] Deprecate Cursor.Error access (#1117) Fix #1111. --- pymysql/cursors.py | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/pymysql/cursors.py b/pymysql/cursors.py index e098e7de..84564a08 100644 --- a/pymysql/cursors.py +++ b/pymysql/cursors.py @@ -1,4 +1,5 @@ import re +import warnings from . import err @@ -352,16 +353,29 @@ def __next__(self): raise StopIteration return row - Warning = err.Warning - Error = err.Error - InterfaceError = err.InterfaceError - DatabaseError = err.DatabaseError - DataError = err.DataError - OperationalError = err.OperationalError - IntegrityError = err.IntegrityError - InternalError = err.InternalError - ProgrammingError = err.ProgrammingError - NotSupportedError = err.NotSupportedError + def __getattr__(self, name): + # DB-API 2.0 optional extension says these errors can be accessed + # via Connection object. But MySQLdb had defined them on Cursor object. + if name in ( + "Warning", + "Error", + "InterfaceError", + "DatabaseError", + "DataError", + "OperationalError", + "IntegrityError", + "InternalError", + "ProgrammingError", + "NotSupportedError", + ): + # Deprecated since v1.1 + warnings.warn( + "PyMySQL errors hould be accessed from `pymysql` package", + DeprecationWarning, + stacklevel=2, + ) + return getattr(err, name) + raise AttributeError(name) class DictCursorMixin: From 103004d6ed59d8eef95fe069e8ca4f60d4965be3 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Thu, 25 May 2023 01:02:27 +0900 Subject: [PATCH 21/80] Run pyupgrade (#1118) --- pymysql/charset.py | 2 +- pymysql/connections.py | 20 +++++++++++--------- pymysql/cursors.py | 4 ++-- pymysql/protocol.py | 4 ++-- pymysql/tests/base.py | 4 ++-- pymysql/tests/test_DictCursor.py | 4 ++-- pymysql/tests/test_basic.py | 2 +- pymysql/tests/test_connection.py | 4 ++-- pymysql/tests/test_cursor.py | 2 +- pymysql/tests/test_issues.py | 4 ++-- 10 files changed, 26 insertions(+), 24 deletions(-) diff --git a/pymysql/charset.py b/pymysql/charset.py index ac87c53d..cdc02164 100644 --- a/pymysql/charset.py +++ b/pymysql/charset.py @@ -7,7 +7,7 @@ def __init__(self, id, name, collation, is_default): self.is_default = is_default == "Yes" def __repr__(self): - return "Charset(id=%s, name=%r, collation=%r)" % ( + return "Charset(id={}, name={!r}, collation={!r})".format( self.id, self.name, self.collation, diff --git a/pymysql/connections.py b/pymysql/connections.py index ef3342aa..d161e789 100644 --- a/pymysql/connections.py +++ b/pymysql/connections.py @@ -528,7 +528,9 @@ def escape_string(self, s): def _quote_bytes(self, s): if self.server_status & SERVER_STATUS.SERVER_STATUS_NO_BACKSLASH_ESCAPES: - return "'%s'" % (s.replace(b"'", b"''").decode("ascii", "surrogateescape"),) + return "'{}'".format( + s.replace(b"'", b"''").decode("ascii", "surrogateescape") + ) return converters.escape_bytes(s) def cursor(self, cursor=None): @@ -621,7 +623,7 @@ def connect(self, sock=None): (self.host, self.port), self.connect_timeout, **kwargs ) break - except (OSError, IOError) as e: + except OSError as e: if e.errno == errno.EINTR: continue raise @@ -662,7 +664,7 @@ def connect(self, sock=None): if isinstance(e, (OSError, IOError)): exc = err.OperationalError( CR.CR_CONN_HOST_ERROR, - "Can't connect to MySQL server on %r (%s)" % (self.host, e), + f"Can't connect to MySQL server on {self.host!r} ({e})", ) # Keep original exception and traceback to investigate error. exc.original_exception = e @@ -739,13 +741,13 @@ def _read_bytes(self, num_bytes): try: data = self._rfile.read(num_bytes) break - except (IOError, OSError) as e: + except OSError as e: if e.errno == errno.EINTR: continue self._force_close() raise err.OperationalError( CR.CR_SERVER_LOST, - "Lost connection to MySQL server during query (%s)" % (e,), + f"Lost connection to MySQL server during query ({e})", ) except BaseException: # Don't convert unknown exception to MySQLError. @@ -762,10 +764,10 @@ def _write_bytes(self, data): self._sock.settimeout(self._write_timeout) try: self._sock.sendall(data) - except IOError as e: + except OSError as e: self._force_close() raise err.OperationalError( - CR.CR_SERVER_GONE_ERROR, "MySQL server has gone away (%r)" % (e,) + CR.CR_SERVER_GONE_ERROR, f"MySQL server has gone away ({e!r})" ) def _read_query_result(self, unbuffered=False): @@ -1006,7 +1008,7 @@ def _process_auth(self, plugin_name, auth_packet): else: raise err.OperationalError( CR.CR_AUTH_PLUGIN_CANNOT_LOAD, - "Authentication plugin '%s' not configured" % (plugin_name,), + f"Authentication plugin '{plugin_name}' not configured", ) pkt = self._read_packet() pkt.check_error() @@ -1382,7 +1384,7 @@ def send_data(self): if not chunk: break conn.write_packet(chunk) - except IOError: + except OSError: raise err.OperationalError( ER.FILE_NOT_FOUND, f"Can't find file '{self.filename}'", diff --git a/pymysql/cursors.py b/pymysql/cursors.py index 84564a08..8be05ca2 100644 --- a/pymysql/cursors.py +++ b/pymysql/cursors.py @@ -262,7 +262,7 @@ def callproc(self, procname, args=()): ) self.nextset() - q = "CALL %s(%s)" % ( + q = "CALL {}({})".format( procname, ",".join(["@_%s_%d" % (procname, i) for i in range(len(args))]), ) @@ -383,7 +383,7 @@ class DictCursorMixin: dict_type = dict def _do_get_result(self): - super(DictCursorMixin, self)._do_get_result() + super()._do_get_result() fields = [] if self.description: for f in self._result.fields: diff --git a/pymysql/protocol.py b/pymysql/protocol.py index 41c81673..2db92d39 100644 --- a/pymysql/protocol.py +++ b/pymysql/protocol.py @@ -35,7 +35,7 @@ def printable(data): dump_data = [data[i : i + 16] for i in range(0, min(len(data), 256), 16)] for d in dump_data: print( - " ".join("{:02X}".format(x) for x in d) + " ".join(f"{x:02X}" for x in d) + " " * (16 - len(d)) + " " * 2 + "".join(printable(x) for x in d) @@ -275,7 +275,7 @@ def get_column_length(self): return self.length def __str__(self): - return "%s %r.%r.%r, type=%s, flags=%x" % ( + return "{} {!r}.{!r}.{!r}, type={}, flags={:x}".format( self.__class__, self.db, self.table_name, diff --git a/pymysql/tests/base.py b/pymysql/tests/base.py index b5094563..6dfa9590 100644 --- a/pymysql/tests/base.py +++ b/pymysql/tests/base.py @@ -98,7 +98,7 @@ def safe_create_table(self, connection, tablename, ddl, cleanup=True): with warnings.catch_warnings(): warnings.simplefilter("ignore") - cursor.execute("drop table if exists `%s`" % (tablename,)) + cursor.execute(f"drop table if exists `{tablename}`") cursor.execute(ddl) cursor.close() if cleanup: @@ -108,5 +108,5 @@ def drop_table(self, connection, tablename): cursor = connection.cursor() with warnings.catch_warnings(): warnings.simplefilter("ignore") - cursor.execute("drop table if exists `%s`" % (tablename,)) + cursor.execute(f"drop table if exists `{tablename}`") cursor.close() diff --git a/pymysql/tests/test_DictCursor.py b/pymysql/tests/test_DictCursor.py index bbc87d03..4e545792 100644 --- a/pymysql/tests/test_DictCursor.py +++ b/pymysql/tests/test_DictCursor.py @@ -13,7 +13,7 @@ class TestDictCursor(base.PyMySQLTestCase): cursor_type = pymysql.cursors.DictCursor def setUp(self): - super(TestDictCursor, self).setUp() + super().setUp() self.conn = conn = self.connect() c = conn.cursor(self.cursor_type) @@ -36,7 +36,7 @@ def setUp(self): def tearDown(self): c = self.conn.cursor() c.execute("drop table dictcursor") - super(TestDictCursor, self).tearDown() + super().tearDown() def _ensure_cursor_expired(self, cursor): pass diff --git a/pymysql/tests/test_basic.py b/pymysql/tests/test_basic.py index ecf043f6..e77605fd 100644 --- a/pymysql/tests/test_basic.py +++ b/pymysql/tests/test_basic.py @@ -332,7 +332,7 @@ class TestBulkInserts(base.PyMySQLTestCase): cursor_type = pymysql.cursors.DictCursor def setUp(self): - super(TestBulkInserts, self).setUp() + super().setUp() self.conn = conn = self.connect() # create a table and some data to query diff --git a/pymysql/tests/test_connection.py b/pymysql/tests/test_connection.py index bbaf3dec..869ff0f8 100644 --- a/pymysql/tests/test_connection.py +++ b/pymysql/tests/test_connection.py @@ -28,7 +28,7 @@ def __init__(self, c, user, db, auth=None, authdata=None, password=None): # already exists - TODO need to check the same plugin applies self._created = False try: - c.execute("GRANT SELECT ON %s.* TO %s" % (db, user)) + c.execute(f"GRANT SELECT ON {db}.* TO {user}") self._grant = True except pymysql.err.InternalError: self._grant = False @@ -38,7 +38,7 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, traceback): if self._grant: - self._c.execute("REVOKE SELECT ON %s.* FROM %s" % (self._db, self._user)) + self._c.execute(f"REVOKE SELECT ON {self._db}.* FROM {self._user}") if self._created: self._c.execute("DROP USER %s" % self._user) diff --git a/pymysql/tests/test_cursor.py b/pymysql/tests/test_cursor.py index 6666ab88..b292c206 100644 --- a/pymysql/tests/test_cursor.py +++ b/pymysql/tests/test_cursor.py @@ -7,7 +7,7 @@ class CursorTest(base.PyMySQLTestCase): def setUp(self): - super(CursorTest, self).setUp() + super().setUp() conn = self.connect() self.safe_create_table( diff --git a/pymysql/tests/test_issues.py b/pymysql/tests/test_issues.py index 7f361c94..3564d3a6 100644 --- a/pymysql/tests/test_issues.py +++ b/pymysql/tests/test_issues.py @@ -379,8 +379,8 @@ def test_issue_175(self): conn = self.connect() cur = conn.cursor() for length in (200, 300): - columns = ", ".join("c{0} integer".format(i) for i in range(length)) - sql = "create table test_field_count ({0})".format(columns) + columns = ", ".join(f"c{i} integer" for i in range(length)) + sql = f"create table test_field_count ({columns})" try: cur.execute(sql) cur.execute("select * from test_field_count") From 69290924144f961167c257ae33959c46e298efd2 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Thu, 25 May 2023 02:01:00 +0900 Subject: [PATCH 22/80] Add `collation` option and `set_character_set()` to Connection (#1119) Send `SET NAMES` on every new connection to ensure charset/collation are correctly configured. Fix #1092 --- pymysql/connections.py | 43 +++++++++++++++++++++++++++++--- pymysql/tests/test_connection.py | 14 +++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/pymysql/connections.py b/pymysql/connections.py index d161e789..f4782939 100644 --- a/pymysql/connections.py +++ b/pymysql/connections.py @@ -112,7 +112,8 @@ class Connection: (default: None - no timeout) :param write_timeout: The timeout for writing to the connection in seconds. (default: None - no timeout) - :param charset: Charset to use. + :param str charset: Charset to use. + :param str collation: Collation name to use. :param sql_mode: Default SQL_MODE to use. :param read_default_file: Specifies my.cnf file to read these parameters from under the [client] section. @@ -174,6 +175,7 @@ def __init__( unix_socket=None, port=0, charset="", + collation=None, sql_mode=None, read_default_file=None, conv=None, @@ -308,6 +310,7 @@ def _config(key, arg): self._write_timeout = write_timeout self.charset = charset or DEFAULT_CHARSET + self.collation = collation self.use_unicode = use_unicode self.encoding = charset_by_name(self.charset).encoding @@ -593,13 +596,32 @@ def ping(self, reconnect=True): raise def set_charset(self, charset): + """Deprecated. Use set_character_set() instead.""" + # This function has been implemented in old PyMySQL. + # But this name is different from MySQLdb. + # So we keep this function for compatibility and add + # new set_character_set() function. + self.set_character_set(charset) + + def set_character_set(self, charset, collation=None): + """ + Set charaset (and collation) + + Send "SET NAMES charset [COLLATE collation]" query. + Update Connection.encoding based on charset. + """ # Make sure charset is supported. encoding = charset_by_name(charset).encoding - self._execute_command(COMMAND.COM_QUERY, "SET NAMES %s" % self.escape(charset)) + if collation: + query = f"SET NAMES {charset} COLLATE {collation}" + else: + query = f"SET NAMES {charset}" + self._execute_command(COMMAND.COM_QUERY, query) self._read_packet() self.charset = charset self.encoding = encoding + self.collation = collation def connect(self, sock=None): self._closed = False @@ -641,15 +663,30 @@ def connect(self, sock=None): self._get_server_information() self._request_authentication() + # Send "SET NAMES" query on init for: + # - Ensure charaset (and collation) is set to the server. + # - collation_id in handshake packet may be ignored. + # - If collation is not specified, we don't know what is server's + # default collation for the charset. For example, default collation + # of utf8mb4 is: + # - MySQL 5.7, MariaDB 10.x: utf8mb4_general_ci + # - MySQL 8.0: utf8mb4_0900_ai_ci + # + # Reference: + # - https://github.com/PyMySQL/PyMySQL/issues/1092 + # - https://github.com/wagtail/wagtail/issues/9477 + # - https://zenn.dev/methane/articles/2023-mysql-collation (Japanese) + self.set_character_set(self.charset, self.collation) + if self.sql_mode is not None: c = self.cursor() c.execute("SET sql_mode=%s", (self.sql_mode,)) + c.close() if self.init_command is not None: c = self.cursor() c.execute(self.init_command) c.close() - self.commit() if self.autocommit_mode is not None: self.autocommit(self.autocommit_mode) diff --git a/pymysql/tests/test_connection.py b/pymysql/tests/test_connection.py index 869ff0f8..0803efc9 100644 --- a/pymysql/tests/test_connection.py +++ b/pymysql/tests/test_connection.py @@ -444,6 +444,20 @@ def test_utf8mb4(self): arg["charset"] = "utf8mb4" pymysql.connect(**arg) + def test_set_character_set(self): + con = self.connect() + cur = con.cursor() + + con.set_character_set("latin1") + cur.execute("SELECT @@character_set_connection") + self.assertEqual(cur.fetchone(), ("latin1",)) + self.assertEqual(con.encoding, "cp1252") + + con.set_character_set("utf8mb4", "utf8mb4_general_ci") + cur.execute("SELECT @@character_set_connection, @@collation_connection") + self.assertEqual(cur.fetchone(), ("utf8mb4", "utf8mb4_general_ci")) + self.assertEqual(con.encoding, "utf8") + def test_largedata(self): """Large query and response (>=16MB)""" cur = self.connect().cursor() From fee5df0397ae99af8def8225b450e25002b8cb13 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Thu, 25 May 2023 14:55:46 +0900 Subject: [PATCH 23/80] CI: Run Django test (#1121) There are some known difference between them so we can not pass the test for now. Fix #1100 --- .github/workflows/django.yaml | 66 +++++++++++++++++++++++++++++++++++ ci/test_mysql.py | 47 +++++++++++++++++++++++++ pymysql/__init__.py | 51 +++++++++++++-------------- pymysql/connections.py | 4 +-- 4 files changed, 140 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/django.yaml create mode 100644 ci/test_mysql.py diff --git a/.github/workflows/django.yaml b/.github/workflows/django.yaml new file mode 100644 index 00000000..da664f85 --- /dev/null +++ b/.github/workflows/django.yaml @@ -0,0 +1,66 @@ +name: Django test + +on: + push: + # branches: ["main"] + # pull_request: + +jobs: + django-test: + name: "Run Django LTS test suite" + runs-on: ubuntu-latest + # There are some known difference between MySQLdb and PyMySQL. + continue-on-error: true + env: + PIP_NO_PYTHON_VERSION_WARNING: 1 + PIP_DISABLE_PIP_VERSION_CHECK: 1 + # DJANGO_VERSION: "3.2.19" + strategy: + fail-fast: false + matrix: + include: + # Django 3.2.9+ supports Python 3.10 + # https://docs.djangoproject.com/ja/3.2/releases/3.2/ + - django: "3.2.19" + python: "3.10" + + - django: "4.2.1" + python: "3.11" + + 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;" + + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - name: Install mysqlclient + run: | + #pip install mysqlclient # Use stable version + pip install .[rsa] + + - name: Setup Django + run: | + sudo apt-get install libmemcached-dev + wget https://github.com/django/django/archive/${{ matrix.django }}.tar.gz + tar xf ${{ matrix.django }}.tar.gz + cp ci/test_mysql.py django-${{ matrix.django }}/tests/ + cd django-${{ matrix.django }} + pip install . -r tests/requirements/py3.txt + + - name: Run Django test + run: | + cd django-${{ matrix.django }}/tests/ + # test_runner does not using our test_mysql.py + # We can't run whole django test suite for now. + # Run olly backends test + DJANGO_SETTINGS_MODULE=test_mysql python runtests.py backends diff --git a/ci/test_mysql.py b/ci/test_mysql.py new file mode 100644 index 00000000..b97978a2 --- /dev/null +++ b/ci/test_mysql.py @@ -0,0 +1,47 @@ +# This is an example test settings file for use with the Django test suite. +# +# The 'sqlite3' backend requires only the ENGINE setting (an in- +# memory database will be used). All other backends will require a +# NAME and potentially authentication information. See the +# following section in the docs for more information: +# +# https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/unit-tests/ +# +# The different databases that Django supports behave differently in certain +# situations, so it is recommended to run the test suite against as many +# database backends as possible. You may want to create a separate settings +# file for each of the backends you test against. + +import pymysql + +pymysql.install_as_MySQLdb() + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.mysql", + "NAME": "django_default", + "HOST": "127.0.0.1", + "USER": "scott", + "PASSWORD": "tiger", + "TEST": {"CHARSET": "utf8mb3", "COLLATION": "utf8mb3_general_ci"}, + }, + "other": { + "ENGINE": "django.db.backends.mysql", + "NAME": "django_other", + "HOST": "127.0.0.1", + "USER": "scott", + "PASSWORD": "tiger", + "TEST": {"CHARSET": "utf8mb3", "COLLATION": "utf8mb3_general_ci"}, + }, +} + +SECRET_KEY = "django_tests_secret_key" + +# Use a fast hasher to speed up tests. +PASSWORD_HASHERS = [ + "django.contrib.auth.hashers.MD5PasswordHasher", +] + +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" + +USE_TZ = False diff --git a/pymysql/__init__.py b/pymysql/__init__.py index c0039c3f..ab43c1a9 100644 --- a/pymysql/__init__.py +++ b/pymysql/__init__.py @@ -46,12 +46,30 @@ TimestampFromTicks, ) +# PyMySQL version. +# Used by setuptools. +VERSION = (1, 1, 0, "dev", 1) + +### for mysqlclient compatibility +### Django checks mysqlclient version. +version_info = (1, 4, 3, "final", 0) +__version__ = "1.4.3" + + +def get_client_info(): # for MySQLdb compatibility + return __version__ + + +def install_as_MySQLdb(): + """ + After this function is called, any application that imports MySQLdb + will unwittingly actually use pymysql. + """ + sys.modules["MySQLdb"] = sys.modules["pymysql"] + + +# end of mysqlclient compatibility code -VERSION = (1, 0, 3) -if len(VERSION) > 3: - VERSION_STRING = "%d.%d.%d_%s" % VERSION -else: - VERSION_STRING = "%d.%d.%d" % VERSION threadsafety = 1 apilevel = "2.0" paramstyle = "pyformat" @@ -109,31 +127,12 @@ def Binary(x): return bytes(x) -Connect = connect = Connection = connections.Connection - - -def get_client_info(): # for MySQLdb compatibility - return VERSION_STRING - - -# we include a doctored version_info here for MySQLdb compatibility -version_info = (1, 4, 0, "final", 0) - -NULL = "NULL" - -__version__ = get_client_info() - - def thread_safe(): return True # match MySQLdb.thread_safe() -def install_as_MySQLdb(): - """ - After this function is called, any application that imports MySQLdb or - _mysql will unwittingly actually use pymysql. - """ - sys.modules["MySQLdb"] = sys.modules["_mysql"] = sys.modules["pymysql"] +Connect = connect = Connection = connections.Connection +NULL = "NULL" __all__ = [ diff --git a/pymysql/connections.py b/pymysql/connections.py index f4782939..6edac04c 100644 --- a/pymysql/connections.py +++ b/pymysql/connections.py @@ -25,7 +25,7 @@ EOFPacketWrapper, LoadLocalPacketWrapper, ) -from . import err, VERSION_STRING +from . import err, __version__ try: import ssl @@ -346,7 +346,7 @@ def _config(key, arg): self._connect_attrs = { "_client_name": "pymysql", "_pid": str(os.getpid()), - "_client_version": VERSION_STRING, + "_client_version": __version__, } if program_name: From a5849526821c2d085b94e25ef0b2499ae04dad84 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Thu, 25 May 2023 15:02:25 +0900 Subject: [PATCH 24/80] Update CHANGELOG --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dc75225..0e94843c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,17 @@ Release date: TBD ## v1.0.3 -Release date: TBD +Release date: 2023-03-28 * Dropped support of end of life MySQL version 5.6 * Dropped support of end of life MariaDB versions below 10.3 * Dropped support of end of life Python version 3.6 +* Removed _last_executed because of duplication with _executed by @rajat315315 in https://github.com/PyMySQL/PyMySQL/pull/948 +* Fix generating authentication response with long strings by @netch80 in https://github.com/PyMySQL/PyMySQL/pull/988 +* update pymysql.constants.CR by @Nothing4You in https://github.com/PyMySQL/PyMySQL/pull/1029 +* Document that the ssl connection parameter can be an SSLContext by @cakemanny in https://github.com/PyMySQL/PyMySQL/pull/1045 +* Raise ProgrammingError on -np.inf in addition to np.inf by @cdcadman in https://github.com/PyMySQL/PyMySQL/pull/1067 +* Use Python 3.11 release instead of -dev in tests by @Nothing4You in https://github.com/PyMySQL/PyMySQL/pull/1076 ## v1.0.2 From 2596bbb5b796aae5bb0759b403d6d28cc22b720c Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Thu, 25 May 2023 15:26:39 +0900 Subject: [PATCH 25/80] Release v1.1.0rc1 (#1122) --- CHANGELOG.md | 8 ++++++++ pymysql/__init__.py | 5 +++-- pymysql/connections.py | 4 ++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e94843c..dc5ff161 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ Release date: TBD * Fixed SSCursor raising OperationalError for query timeouts on wrong statement (#1032) * Exposed `Cursor.warning_count` to check for warnings without additional query (#1056) +* Make Cursor iterator (#995) +* Support '_' in key name in my.cnf (#1114) +* `Cursor.fetchall()` returns empty list instead of tuple (#1115). Note that `Cursor.fetchmany()` still return empty tuple after reading all rows for compatibility with Django. +* Deprecate Error classes in Cursor class (#1117) +* Add `Connection.set_character_set(charset, collation=None)` (#1119) +* Deprecate `Connection.set_charset(charset)` (#1119) +* New connection always send "SET NAMES charset [COLLATE collation]" query. (#1119) + Since collation table is vary on MySQL server versions, collation in handshake is fragile. ## v1.0.3 diff --git a/pymysql/__init__.py b/pymysql/__init__.py index ab43c1a9..b9971ff0 100644 --- a/pymysql/__init__.py +++ b/pymysql/__init__.py @@ -47,8 +47,9 @@ ) # PyMySQL version. -# Used by setuptools. -VERSION = (1, 1, 0, "dev", 1) +# Used by setuptools and connection_attrs +VERSION = (1, 1, 0, "rc", 1) +VERSION_STRING = "1.1.0rc1" ### for mysqlclient compatibility ### Django checks mysqlclient version. diff --git a/pymysql/connections.py b/pymysql/connections.py index 6edac04c..843bea5e 100644 --- a/pymysql/connections.py +++ b/pymysql/connections.py @@ -25,7 +25,7 @@ EOFPacketWrapper, LoadLocalPacketWrapper, ) -from . import err, __version__ +from . import err, VERSION_STRING try: import ssl @@ -345,8 +345,8 @@ def _config(key, arg): self._connect_attrs = { "_client_name": "pymysql", + "_client_version": VERSION_STRING, "_pid": str(os.getpid()), - "_client_version": __version__, } if program_name: From 2df6c068b7a0dd733e72a068b3aca3e8738177ad Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Thu, 25 May 2023 17:36:52 +1000 Subject: [PATCH 26/80] Bump mariadb version (#1123) In README and GH actions. --- .github/workflows/test.yaml | 6 +++--- README.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c3275cca..6b1e0f32 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -15,16 +15,16 @@ jobs: fail-fast: false matrix: include: - - db: "mariadb:10.3" + - db: "mariadb:10.4" py: "3.8" - db: "mariadb:10.5" py: "3.7" - - db: "mariadb:10.7" + - db: "mariadb:10.6" py: "3.11" - - db: "mariadb:10.8" + - db: "mariadb:lts" py: "3.9" - db: "mysql:5.7" diff --git a/README.md b/README.md index 6e6a6bf2..32f5df2f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This package contains a pure-Python MySQL client library, based on [PEP - [PyPy](https://pypy.org/) : Latest 3.x version - MySQL Server -- one of the following: - [MySQL](https://www.mysql.com/) \>= 5.7 - - [MariaDB](https://mariadb.org/) \>= 10.3 + - [MariaDB](https://mariadb.org/) \>= 10.4 ## Installation From f4c348fdcf4ac21a92be58b6f94e9d7a13826a38 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 Jun 2023 13:55:38 +0900 Subject: [PATCH 27/80] Configure Renovate (#1124) --- renovate.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..39a2b6e9 --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ] +} From c3a12f683345a97a8cc8516cf2123a5836c38f7d Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Thu, 15 Jun 2023 13:20:26 +0900 Subject: [PATCH 28/80] Make charset="utf8" use utf8mb4. (#1127) Use charset="utf8mb3" to use utf8mb3 instead. Fix #1126 --- pymysql/charset.py | 319 +++++++++++++++++----------------- pymysql/tests/test_charset.py | 25 +++ 2 files changed, 188 insertions(+), 156 deletions(-) create mode 100644 pymysql/tests/test_charset.py diff --git a/pymysql/charset.py b/pymysql/charset.py index cdc02164..b1c1ca8b 100644 --- a/pymysql/charset.py +++ b/pymysql/charset.py @@ -1,16 +1,16 @@ +# Internal use only. Do not use directly. + MBLENGTH = {8: 1, 33: 3, 88: 2, 91: 2} class Charset: - def __init__(self, id, name, collation, is_default): + def __init__(self, id, name, collation, is_default=False): self.id, self.name, self.collation = id, name, collation - self.is_default = is_default == "Yes" + self.is_default = is_default def __repr__(self): - return "Charset(id={}, name={!r}, collation={!r})".format( - self.id, - self.name, - self.collation, + return ( + f"Charset(id={self.id}, name={self.name!r}, collation={self.collation!r})" ) @property @@ -45,165 +45,172 @@ def by_id(self, id): return self._by_id[id] def by_name(self, name): + if name == "utf8": + name = "utf8mb4" return self._by_name.get(name.lower()) _charsets = Charsets() +charset_by_name = _charsets.by_name +charset_by_id = _charsets.by_id + """ +TODO: update this script. + Generated with: mysql -N -s -e "select id, character_set_name, collation_name, is_default from information_schema.collations order by id;" | python -c "import sys for l in sys.stdin.readlines(): - id, name, collation, is_default = l.split(chr(9)) - print '_charsets.add(Charset(%s, \'%s\', \'%s\', \'%s\'))' \ - % (id, name, collation, is_default.strip()) -" - + id, name, collation, is_default = l.split(chr(9)) + if is_default.strip() == "Yes": + print('_charsets.add(Charset(%s, \'%s\', \'%s\', True))' \ + % (id, name, collation)) + else: + print('_charsets.add(Charset(%s, \'%s\', \'%s\'))' \ + % (id, name, collation, bool(is_default.strip())) """ -_charsets.add(Charset(1, "big5", "big5_chinese_ci", "Yes")) -_charsets.add(Charset(2, "latin2", "latin2_czech_cs", "")) -_charsets.add(Charset(3, "dec8", "dec8_swedish_ci", "Yes")) -_charsets.add(Charset(4, "cp850", "cp850_general_ci", "Yes")) -_charsets.add(Charset(5, "latin1", "latin1_german1_ci", "")) -_charsets.add(Charset(6, "hp8", "hp8_english_ci", "Yes")) -_charsets.add(Charset(7, "koi8r", "koi8r_general_ci", "Yes")) -_charsets.add(Charset(8, "latin1", "latin1_swedish_ci", "Yes")) -_charsets.add(Charset(9, "latin2", "latin2_general_ci", "Yes")) -_charsets.add(Charset(10, "swe7", "swe7_swedish_ci", "Yes")) -_charsets.add(Charset(11, "ascii", "ascii_general_ci", "Yes")) -_charsets.add(Charset(12, "ujis", "ujis_japanese_ci", "Yes")) -_charsets.add(Charset(13, "sjis", "sjis_japanese_ci", "Yes")) -_charsets.add(Charset(14, "cp1251", "cp1251_bulgarian_ci", "")) -_charsets.add(Charset(15, "latin1", "latin1_danish_ci", "")) -_charsets.add(Charset(16, "hebrew", "hebrew_general_ci", "Yes")) -_charsets.add(Charset(18, "tis620", "tis620_thai_ci", "Yes")) -_charsets.add(Charset(19, "euckr", "euckr_korean_ci", "Yes")) -_charsets.add(Charset(20, "latin7", "latin7_estonian_cs", "")) -_charsets.add(Charset(21, "latin2", "latin2_hungarian_ci", "")) -_charsets.add(Charset(22, "koi8u", "koi8u_general_ci", "Yes")) -_charsets.add(Charset(23, "cp1251", "cp1251_ukrainian_ci", "")) -_charsets.add(Charset(24, "gb2312", "gb2312_chinese_ci", "Yes")) -_charsets.add(Charset(25, "greek", "greek_general_ci", "Yes")) -_charsets.add(Charset(26, "cp1250", "cp1250_general_ci", "Yes")) -_charsets.add(Charset(27, "latin2", "latin2_croatian_ci", "")) -_charsets.add(Charset(28, "gbk", "gbk_chinese_ci", "Yes")) -_charsets.add(Charset(29, "cp1257", "cp1257_lithuanian_ci", "")) -_charsets.add(Charset(30, "latin5", "latin5_turkish_ci", "Yes")) -_charsets.add(Charset(31, "latin1", "latin1_german2_ci", "")) -_charsets.add(Charset(32, "armscii8", "armscii8_general_ci", "Yes")) -_charsets.add(Charset(33, "utf8", "utf8_general_ci", "Yes")) -_charsets.add(Charset(34, "cp1250", "cp1250_czech_cs", "")) -_charsets.add(Charset(36, "cp866", "cp866_general_ci", "Yes")) -_charsets.add(Charset(37, "keybcs2", "keybcs2_general_ci", "Yes")) -_charsets.add(Charset(38, "macce", "macce_general_ci", "Yes")) -_charsets.add(Charset(39, "macroman", "macroman_general_ci", "Yes")) -_charsets.add(Charset(40, "cp852", "cp852_general_ci", "Yes")) -_charsets.add(Charset(41, "latin7", "latin7_general_ci", "Yes")) -_charsets.add(Charset(42, "latin7", "latin7_general_cs", "")) -_charsets.add(Charset(43, "macce", "macce_bin", "")) -_charsets.add(Charset(44, "cp1250", "cp1250_croatian_ci", "")) -_charsets.add(Charset(45, "utf8mb4", "utf8mb4_general_ci", "Yes")) -_charsets.add(Charset(46, "utf8mb4", "utf8mb4_bin", "")) -_charsets.add(Charset(47, "latin1", "latin1_bin", "")) -_charsets.add(Charset(48, "latin1", "latin1_general_ci", "")) -_charsets.add(Charset(49, "latin1", "latin1_general_cs", "")) -_charsets.add(Charset(50, "cp1251", "cp1251_bin", "")) -_charsets.add(Charset(51, "cp1251", "cp1251_general_ci", "Yes")) -_charsets.add(Charset(52, "cp1251", "cp1251_general_cs", "")) -_charsets.add(Charset(53, "macroman", "macroman_bin", "")) -_charsets.add(Charset(57, "cp1256", "cp1256_general_ci", "Yes")) -_charsets.add(Charset(58, "cp1257", "cp1257_bin", "")) -_charsets.add(Charset(59, "cp1257", "cp1257_general_ci", "Yes")) -_charsets.add(Charset(63, "binary", "binary", "Yes")) -_charsets.add(Charset(64, "armscii8", "armscii8_bin", "")) -_charsets.add(Charset(65, "ascii", "ascii_bin", "")) -_charsets.add(Charset(66, "cp1250", "cp1250_bin", "")) -_charsets.add(Charset(67, "cp1256", "cp1256_bin", "")) -_charsets.add(Charset(68, "cp866", "cp866_bin", "")) -_charsets.add(Charset(69, "dec8", "dec8_bin", "")) -_charsets.add(Charset(70, "greek", "greek_bin", "")) -_charsets.add(Charset(71, "hebrew", "hebrew_bin", "")) -_charsets.add(Charset(72, "hp8", "hp8_bin", "")) -_charsets.add(Charset(73, "keybcs2", "keybcs2_bin", "")) -_charsets.add(Charset(74, "koi8r", "koi8r_bin", "")) -_charsets.add(Charset(75, "koi8u", "koi8u_bin", "")) -_charsets.add(Charset(76, "utf8", "utf8_tolower_ci", "")) -_charsets.add(Charset(77, "latin2", "latin2_bin", "")) -_charsets.add(Charset(78, "latin5", "latin5_bin", "")) -_charsets.add(Charset(79, "latin7", "latin7_bin", "")) -_charsets.add(Charset(80, "cp850", "cp850_bin", "")) -_charsets.add(Charset(81, "cp852", "cp852_bin", "")) -_charsets.add(Charset(82, "swe7", "swe7_bin", "")) -_charsets.add(Charset(83, "utf8", "utf8_bin", "")) -_charsets.add(Charset(84, "big5", "big5_bin", "")) -_charsets.add(Charset(85, "euckr", "euckr_bin", "")) -_charsets.add(Charset(86, "gb2312", "gb2312_bin", "")) -_charsets.add(Charset(87, "gbk", "gbk_bin", "")) -_charsets.add(Charset(88, "sjis", "sjis_bin", "")) -_charsets.add(Charset(89, "tis620", "tis620_bin", "")) -_charsets.add(Charset(91, "ujis", "ujis_bin", "")) -_charsets.add(Charset(92, "geostd8", "geostd8_general_ci", "Yes")) -_charsets.add(Charset(93, "geostd8", "geostd8_bin", "")) -_charsets.add(Charset(94, "latin1", "latin1_spanish_ci", "")) -_charsets.add(Charset(95, "cp932", "cp932_japanese_ci", "Yes")) -_charsets.add(Charset(96, "cp932", "cp932_bin", "")) -_charsets.add(Charset(97, "eucjpms", "eucjpms_japanese_ci", "Yes")) -_charsets.add(Charset(98, "eucjpms", "eucjpms_bin", "")) -_charsets.add(Charset(99, "cp1250", "cp1250_polish_ci", "")) -_charsets.add(Charset(192, "utf8", "utf8_unicode_ci", "")) -_charsets.add(Charset(193, "utf8", "utf8_icelandic_ci", "")) -_charsets.add(Charset(194, "utf8", "utf8_latvian_ci", "")) -_charsets.add(Charset(195, "utf8", "utf8_romanian_ci", "")) -_charsets.add(Charset(196, "utf8", "utf8_slovenian_ci", "")) -_charsets.add(Charset(197, "utf8", "utf8_polish_ci", "")) -_charsets.add(Charset(198, "utf8", "utf8_estonian_ci", "")) -_charsets.add(Charset(199, "utf8", "utf8_spanish_ci", "")) -_charsets.add(Charset(200, "utf8", "utf8_swedish_ci", "")) -_charsets.add(Charset(201, "utf8", "utf8_turkish_ci", "")) -_charsets.add(Charset(202, "utf8", "utf8_czech_ci", "")) -_charsets.add(Charset(203, "utf8", "utf8_danish_ci", "")) -_charsets.add(Charset(204, "utf8", "utf8_lithuanian_ci", "")) -_charsets.add(Charset(205, "utf8", "utf8_slovak_ci", "")) -_charsets.add(Charset(206, "utf8", "utf8_spanish2_ci", "")) -_charsets.add(Charset(207, "utf8", "utf8_roman_ci", "")) -_charsets.add(Charset(208, "utf8", "utf8_persian_ci", "")) -_charsets.add(Charset(209, "utf8", "utf8_esperanto_ci", "")) -_charsets.add(Charset(210, "utf8", "utf8_hungarian_ci", "")) -_charsets.add(Charset(211, "utf8", "utf8_sinhala_ci", "")) -_charsets.add(Charset(212, "utf8", "utf8_german2_ci", "")) -_charsets.add(Charset(213, "utf8", "utf8_croatian_ci", "")) -_charsets.add(Charset(214, "utf8", "utf8_unicode_520_ci", "")) -_charsets.add(Charset(215, "utf8", "utf8_vietnamese_ci", "")) -_charsets.add(Charset(223, "utf8", "utf8_general_mysql500_ci", "")) -_charsets.add(Charset(224, "utf8mb4", "utf8mb4_unicode_ci", "")) -_charsets.add(Charset(225, "utf8mb4", "utf8mb4_icelandic_ci", "")) -_charsets.add(Charset(226, "utf8mb4", "utf8mb4_latvian_ci", "")) -_charsets.add(Charset(227, "utf8mb4", "utf8mb4_romanian_ci", "")) -_charsets.add(Charset(228, "utf8mb4", "utf8mb4_slovenian_ci", "")) -_charsets.add(Charset(229, "utf8mb4", "utf8mb4_polish_ci", "")) -_charsets.add(Charset(230, "utf8mb4", "utf8mb4_estonian_ci", "")) -_charsets.add(Charset(231, "utf8mb4", "utf8mb4_spanish_ci", "")) -_charsets.add(Charset(232, "utf8mb4", "utf8mb4_swedish_ci", "")) -_charsets.add(Charset(233, "utf8mb4", "utf8mb4_turkish_ci", "")) -_charsets.add(Charset(234, "utf8mb4", "utf8mb4_czech_ci", "")) -_charsets.add(Charset(235, "utf8mb4", "utf8mb4_danish_ci", "")) -_charsets.add(Charset(236, "utf8mb4", "utf8mb4_lithuanian_ci", "")) -_charsets.add(Charset(237, "utf8mb4", "utf8mb4_slovak_ci", "")) -_charsets.add(Charset(238, "utf8mb4", "utf8mb4_spanish2_ci", "")) -_charsets.add(Charset(239, "utf8mb4", "utf8mb4_roman_ci", "")) -_charsets.add(Charset(240, "utf8mb4", "utf8mb4_persian_ci", "")) -_charsets.add(Charset(241, "utf8mb4", "utf8mb4_esperanto_ci", "")) -_charsets.add(Charset(242, "utf8mb4", "utf8mb4_hungarian_ci", "")) -_charsets.add(Charset(243, "utf8mb4", "utf8mb4_sinhala_ci", "")) -_charsets.add(Charset(244, "utf8mb4", "utf8mb4_german2_ci", "")) -_charsets.add(Charset(245, "utf8mb4", "utf8mb4_croatian_ci", "")) -_charsets.add(Charset(246, "utf8mb4", "utf8mb4_unicode_520_ci", "")) -_charsets.add(Charset(247, "utf8mb4", "utf8mb4_vietnamese_ci", "")) -_charsets.add(Charset(248, "gb18030", "gb18030_chinese_ci", "Yes")) -_charsets.add(Charset(249, "gb18030", "gb18030_bin", "")) -_charsets.add(Charset(250, "gb18030", "gb18030_unicode_520_ci", "")) -_charsets.add(Charset(255, "utf8mb4", "utf8mb4_0900_ai_ci", "")) -charset_by_name = _charsets.by_name -charset_by_id = _charsets.by_id +_charsets.add(Charset(1, "big5", "big5_chinese_ci", True)) +_charsets.add(Charset(2, "latin2", "latin2_czech_cs")) +_charsets.add(Charset(3, "dec8", "dec8_swedish_ci", True)) +_charsets.add(Charset(4, "cp850", "cp850_general_ci", True)) +_charsets.add(Charset(5, "latin1", "latin1_german1_ci")) +_charsets.add(Charset(6, "hp8", "hp8_english_ci", True)) +_charsets.add(Charset(7, "koi8r", "koi8r_general_ci", True)) +_charsets.add(Charset(8, "latin1", "latin1_swedish_ci", True)) +_charsets.add(Charset(9, "latin2", "latin2_general_ci", True)) +_charsets.add(Charset(10, "swe7", "swe7_swedish_ci", True)) +_charsets.add(Charset(11, "ascii", "ascii_general_ci", True)) +_charsets.add(Charset(12, "ujis", "ujis_japanese_ci", True)) +_charsets.add(Charset(13, "sjis", "sjis_japanese_ci", True)) +_charsets.add(Charset(14, "cp1251", "cp1251_bulgarian_ci")) +_charsets.add(Charset(15, "latin1", "latin1_danish_ci")) +_charsets.add(Charset(16, "hebrew", "hebrew_general_ci", True)) +_charsets.add(Charset(18, "tis620", "tis620_thai_ci", True)) +_charsets.add(Charset(19, "euckr", "euckr_korean_ci", True)) +_charsets.add(Charset(20, "latin7", "latin7_estonian_cs")) +_charsets.add(Charset(21, "latin2", "latin2_hungarian_ci")) +_charsets.add(Charset(22, "koi8u", "koi8u_general_ci", True)) +_charsets.add(Charset(23, "cp1251", "cp1251_ukrainian_ci")) +_charsets.add(Charset(24, "gb2312", "gb2312_chinese_ci", True)) +_charsets.add(Charset(25, "greek", "greek_general_ci", True)) +_charsets.add(Charset(26, "cp1250", "cp1250_general_ci", True)) +_charsets.add(Charset(27, "latin2", "latin2_croatian_ci")) +_charsets.add(Charset(28, "gbk", "gbk_chinese_ci", True)) +_charsets.add(Charset(29, "cp1257", "cp1257_lithuanian_ci")) +_charsets.add(Charset(30, "latin5", "latin5_turkish_ci", True)) +_charsets.add(Charset(31, "latin1", "latin1_german2_ci")) +_charsets.add(Charset(32, "armscii8", "armscii8_general_ci", True)) +_charsets.add(Charset(33, "utf8mb3", "utf8mb3_general_ci", True)) +_charsets.add(Charset(34, "cp1250", "cp1250_czech_cs")) +_charsets.add(Charset(36, "cp866", "cp866_general_ci", True)) +_charsets.add(Charset(37, "keybcs2", "keybcs2_general_ci", True)) +_charsets.add(Charset(38, "macce", "macce_general_ci", True)) +_charsets.add(Charset(39, "macroman", "macroman_general_ci", True)) +_charsets.add(Charset(40, "cp852", "cp852_general_ci", True)) +_charsets.add(Charset(41, "latin7", "latin7_general_ci", True)) +_charsets.add(Charset(42, "latin7", "latin7_general_cs")) +_charsets.add(Charset(43, "macce", "macce_bin")) +_charsets.add(Charset(44, "cp1250", "cp1250_croatian_ci")) +_charsets.add(Charset(45, "utf8mb4", "utf8mb4_general_ci", True)) +_charsets.add(Charset(46, "utf8mb4", "utf8mb4_bin")) +_charsets.add(Charset(47, "latin1", "latin1_bin")) +_charsets.add(Charset(48, "latin1", "latin1_general_ci")) +_charsets.add(Charset(49, "latin1", "latin1_general_cs")) +_charsets.add(Charset(50, "cp1251", "cp1251_bin")) +_charsets.add(Charset(51, "cp1251", "cp1251_general_ci", True)) +_charsets.add(Charset(52, "cp1251", "cp1251_general_cs")) +_charsets.add(Charset(53, "macroman", "macroman_bin")) +_charsets.add(Charset(57, "cp1256", "cp1256_general_ci", True)) +_charsets.add(Charset(58, "cp1257", "cp1257_bin")) +_charsets.add(Charset(59, "cp1257", "cp1257_general_ci", True)) +_charsets.add(Charset(63, "binary", "binary", True)) +_charsets.add(Charset(64, "armscii8", "armscii8_bin")) +_charsets.add(Charset(65, "ascii", "ascii_bin")) +_charsets.add(Charset(66, "cp1250", "cp1250_bin")) +_charsets.add(Charset(67, "cp1256", "cp1256_bin")) +_charsets.add(Charset(68, "cp866", "cp866_bin")) +_charsets.add(Charset(69, "dec8", "dec8_bin")) +_charsets.add(Charset(70, "greek", "greek_bin")) +_charsets.add(Charset(71, "hebrew", "hebrew_bin")) +_charsets.add(Charset(72, "hp8", "hp8_bin")) +_charsets.add(Charset(73, "keybcs2", "keybcs2_bin")) +_charsets.add(Charset(74, "koi8r", "koi8r_bin")) +_charsets.add(Charset(75, "koi8u", "koi8u_bin")) +_charsets.add(Charset(76, "utf8mb3", "utf8mb3_tolower_ci")) +_charsets.add(Charset(77, "latin2", "latin2_bin")) +_charsets.add(Charset(78, "latin5", "latin5_bin")) +_charsets.add(Charset(79, "latin7", "latin7_bin")) +_charsets.add(Charset(80, "cp850", "cp850_bin")) +_charsets.add(Charset(81, "cp852", "cp852_bin")) +_charsets.add(Charset(82, "swe7", "swe7_bin")) +_charsets.add(Charset(83, "utf8mb3", "utf8mb3_bin")) +_charsets.add(Charset(84, "big5", "big5_bin")) +_charsets.add(Charset(85, "euckr", "euckr_bin")) +_charsets.add(Charset(86, "gb2312", "gb2312_bin")) +_charsets.add(Charset(87, "gbk", "gbk_bin")) +_charsets.add(Charset(88, "sjis", "sjis_bin")) +_charsets.add(Charset(89, "tis620", "tis620_bin")) +_charsets.add(Charset(91, "ujis", "ujis_bin")) +_charsets.add(Charset(92, "geostd8", "geostd8_general_ci", True)) +_charsets.add(Charset(93, "geostd8", "geostd8_bin")) +_charsets.add(Charset(94, "latin1", "latin1_spanish_ci")) +_charsets.add(Charset(95, "cp932", "cp932_japanese_ci", True)) +_charsets.add(Charset(96, "cp932", "cp932_bin")) +_charsets.add(Charset(97, "eucjpms", "eucjpms_japanese_ci", True)) +_charsets.add(Charset(98, "eucjpms", "eucjpms_bin")) +_charsets.add(Charset(99, "cp1250", "cp1250_polish_ci")) +_charsets.add(Charset(192, "utf8mb3", "utf8mb3_unicode_ci")) +_charsets.add(Charset(193, "utf8mb3", "utf8mb3_icelandic_ci")) +_charsets.add(Charset(194, "utf8mb3", "utf8mb3_latvian_ci")) +_charsets.add(Charset(195, "utf8mb3", "utf8mb3_romanian_ci")) +_charsets.add(Charset(196, "utf8mb3", "utf8mb3_slovenian_ci")) +_charsets.add(Charset(197, "utf8mb3", "utf8mb3_polish_ci")) +_charsets.add(Charset(198, "utf8mb3", "utf8mb3_estonian_ci")) +_charsets.add(Charset(199, "utf8mb3", "utf8mb3_spanish_ci")) +_charsets.add(Charset(200, "utf8mb3", "utf8mb3_swedish_ci")) +_charsets.add(Charset(201, "utf8mb3", "utf8mb3_turkish_ci")) +_charsets.add(Charset(202, "utf8mb3", "utf8mb3_czech_ci")) +_charsets.add(Charset(203, "utf8mb3", "utf8mb3_danish_ci")) +_charsets.add(Charset(204, "utf8mb3", "utf8mb3_lithuanian_ci")) +_charsets.add(Charset(205, "utf8mb3", "utf8mb3_slovak_ci")) +_charsets.add(Charset(206, "utf8mb3", "utf8mb3_spanish2_ci")) +_charsets.add(Charset(207, "utf8mb3", "utf8mb3_roman_ci")) +_charsets.add(Charset(208, "utf8mb3", "utf8mb3_persian_ci")) +_charsets.add(Charset(209, "utf8mb3", "utf8mb3_esperanto_ci")) +_charsets.add(Charset(210, "utf8mb3", "utf8mb3_hungarian_ci")) +_charsets.add(Charset(211, "utf8mb3", "utf8mb3_sinhala_ci")) +_charsets.add(Charset(212, "utf8mb3", "utf8mb3_german2_ci")) +_charsets.add(Charset(213, "utf8mb3", "utf8mb3_croatian_ci")) +_charsets.add(Charset(214, "utf8mb3", "utf8mb3_unicode_520_ci")) +_charsets.add(Charset(215, "utf8mb3", "utf8mb3_vietnamese_ci")) +_charsets.add(Charset(223, "utf8mb3", "utf8mb3_general_mysql500_ci")) +_charsets.add(Charset(224, "utf8mb4", "utf8mb4_unicode_ci")) +_charsets.add(Charset(225, "utf8mb4", "utf8mb4_icelandic_ci")) +_charsets.add(Charset(226, "utf8mb4", "utf8mb4_latvian_ci")) +_charsets.add(Charset(227, "utf8mb4", "utf8mb4_romanian_ci")) +_charsets.add(Charset(228, "utf8mb4", "utf8mb4_slovenian_ci")) +_charsets.add(Charset(229, "utf8mb4", "utf8mb4_polish_ci")) +_charsets.add(Charset(230, "utf8mb4", "utf8mb4_estonian_ci")) +_charsets.add(Charset(231, "utf8mb4", "utf8mb4_spanish_ci")) +_charsets.add(Charset(232, "utf8mb4", "utf8mb4_swedish_ci")) +_charsets.add(Charset(233, "utf8mb4", "utf8mb4_turkish_ci")) +_charsets.add(Charset(234, "utf8mb4", "utf8mb4_czech_ci")) +_charsets.add(Charset(235, "utf8mb4", "utf8mb4_danish_ci")) +_charsets.add(Charset(236, "utf8mb4", "utf8mb4_lithuanian_ci")) +_charsets.add(Charset(237, "utf8mb4", "utf8mb4_slovak_ci")) +_charsets.add(Charset(238, "utf8mb4", "utf8mb4_spanish2_ci")) +_charsets.add(Charset(239, "utf8mb4", "utf8mb4_roman_ci")) +_charsets.add(Charset(240, "utf8mb4", "utf8mb4_persian_ci")) +_charsets.add(Charset(241, "utf8mb4", "utf8mb4_esperanto_ci")) +_charsets.add(Charset(242, "utf8mb4", "utf8mb4_hungarian_ci")) +_charsets.add(Charset(243, "utf8mb4", "utf8mb4_sinhala_ci")) +_charsets.add(Charset(244, "utf8mb4", "utf8mb4_german2_ci")) +_charsets.add(Charset(245, "utf8mb4", "utf8mb4_croatian_ci")) +_charsets.add(Charset(246, "utf8mb4", "utf8mb4_unicode_520_ci")) +_charsets.add(Charset(247, "utf8mb4", "utf8mb4_vietnamese_ci")) +_charsets.add(Charset(248, "gb18030", "gb18030_chinese_ci", True)) +_charsets.add(Charset(249, "gb18030", "gb18030_bin")) +_charsets.add(Charset(250, "gb18030", "gb18030_unicode_520_ci")) +_charsets.add(Charset(255, "utf8mb4", "utf8mb4_0900_ai_ci")) diff --git a/pymysql/tests/test_charset.py b/pymysql/tests/test_charset.py new file mode 100644 index 00000000..94e6e155 --- /dev/null +++ b/pymysql/tests/test_charset.py @@ -0,0 +1,25 @@ +import pymysql.charset + + +def test_utf8(): + utf8mb3 = pymysql.charset.charset_by_name("utf8mb3") + assert utf8mb3.name == "utf8mb3" + assert utf8mb3.collation == "utf8mb3_general_ci" + assert ( + repr(utf8mb3) + == "Charset(id=33, name='utf8mb3', collation='utf8mb3_general_ci')" + ) + + # MySQL 8.0 changed the default collation for utf8mb4. + # But we use old default for compatibility. + utf8mb4 = pymysql.charset.charset_by_name("utf8mb4") + assert utf8mb4.name == "utf8mb4" + assert utf8mb4.collation == "utf8mb4_general_ci" + assert ( + repr(utf8mb4) + == "Charset(id=45, name='utf8mb4', collation='utf8mb4_general_ci')" + ) + + # utf8 is alias of utf8mb4 since MySQL 8.0, and PyMySQL v1.1. + utf8 = pymysql.charset.charset_by_name("utf8") + assert utf8 == utf8mb4 From fed7e8069bf09d3b4e819dc8c59d6b7096e4183f Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Thu, 15 Jun 2023 13:31:21 +0900 Subject: [PATCH 29/80] Add codecov.yml (#1128) --- codecov.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..919adf20 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,7 @@ +# https://docs.codecov.com/docs/common-recipe-list +coverage: + status: + project: + default: + target: auto + threshold: 3% From f3f3477682a3bbc80eb0240034abcd288d7dda63 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Thu, 15 Jun 2023 16:58:17 +0900 Subject: [PATCH 30/80] Release v1.1.0rc2 (#1129) --- CHANGELOG.md | 1 + pymysql/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc5ff161..ea1d732a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Release date: TBD * Deprecate `Connection.set_charset(charset)` (#1119) * New connection always send "SET NAMES charset [COLLATE collation]" query. (#1119) Since collation table is vary on MySQL server versions, collation in handshake is fragile. +* Support `charset="utf8mb3"` option (#1127) ## v1.0.3 diff --git a/pymysql/__init__.py b/pymysql/__init__.py index b9971ff0..68d7043b 100644 --- a/pymysql/__init__.py +++ b/pymysql/__init__.py @@ -48,8 +48,8 @@ # PyMySQL version. # Used by setuptools and connection_attrs -VERSION = (1, 1, 0, "rc", 1) -VERSION_STRING = "1.1.0rc1" +VERSION = (1, 1, 0, "rc", 2) +VERSION_STRING = "1.1.0rc2" ### for mysqlclient compatibility ### Django checks mysqlclient version. From 0803b539d4e370001fc93942643ab6843d3eb331 Mon Sep 17 00:00:00 2001 From: "codesee-maps[bot]" <86324825+codesee-maps[bot]@users.noreply.github.com> Date: Wed, 21 Jun 2023 18:22:40 +0000 Subject: [PATCH 31/80] Install the CodeSee workflow. Learn more at https://docs.codesee.io --- .github/workflows/codesee-arch-diagram.yml | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/codesee-arch-diagram.yml diff --git a/.github/workflows/codesee-arch-diagram.yml b/.github/workflows/codesee-arch-diagram.yml new file mode 100644 index 00000000..806d41d1 --- /dev/null +++ b/.github/workflows/codesee-arch-diagram.yml @@ -0,0 +1,23 @@ +# 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 fe856a55963eac53d5fd714d7de06328cab90293 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Mon, 26 Jun 2023 14:31:53 +0900 Subject: [PATCH 32/80] Release v1.1.0 (#1130) --- CHANGELOG.md | 13 ++++++++++--- pymysql/__init__.py | 8 ++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea1d732a..c6283670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,15 @@ # Changes +## Backward incompatible changes planned in the future. + +* Error classes in Cursor class will be removed after 2024-06 +* `Connection.set_charset(charset)` will be removed after 2024-06 +* `db` and `passwd` will emit DeprecationWarning in v1.2. See #933. + + ## v1.1.0 -Release date: TBD +Release date: 2023-06-26 * Fixed SSCursor raising OperationalError for query timeouts on wrong statement (#1032) * Exposed `Cursor.warning_count` to check for warnings without additional query (#1056) @@ -10,7 +17,7 @@ Release date: TBD * Support '_' in key name in my.cnf (#1114) * `Cursor.fetchall()` returns empty list instead of tuple (#1115). Note that `Cursor.fetchmany()` still return empty tuple after reading all rows for compatibility with Django. * Deprecate Error classes in Cursor class (#1117) -* Add `Connection.set_character_set(charset, collation=None)` (#1119) +* Add `Connection.set_character_set(charset, collation=None)`. This method is compatible with mysqlclient. (#1119) * Deprecate `Connection.set_charset(charset)` (#1119) * New connection always send "SET NAMES charset [COLLATE collation]" query. (#1119) Since collation table is vary on MySQL server versions, collation in handshake is fragile. @@ -24,7 +31,7 @@ Release date: 2023-03-28 * Dropped support of end of life MySQL version 5.6 * Dropped support of end of life MariaDB versions below 10.3 * Dropped support of end of life Python version 3.6 -* Removed _last_executed because of duplication with _executed by @rajat315315 in https://github.com/PyMySQL/PyMySQL/pull/948 +* Removed `_last_executed` because of duplication with `_executed` by @rajat315315 in https://github.com/PyMySQL/PyMySQL/pull/948 * Fix generating authentication response with long strings by @netch80 in https://github.com/PyMySQL/PyMySQL/pull/988 * update pymysql.constants.CR by @Nothing4You in https://github.com/PyMySQL/PyMySQL/pull/1029 * Document that the ssl connection parameter can be an SSLContext by @cakemanny in https://github.com/PyMySQL/PyMySQL/pull/1045 diff --git a/pymysql/__init__.py b/pymysql/__init__.py index 68d7043b..53625d37 100644 --- a/pymysql/__init__.py +++ b/pymysql/__init__.py @@ -48,13 +48,13 @@ # PyMySQL version. # Used by setuptools and connection_attrs -VERSION = (1, 1, 0, "rc", 2) -VERSION_STRING = "1.1.0rc2" +VERSION = (1, 1, 0, "final", 1) +VERSION_STRING = "1.1.0" ### for mysqlclient compatibility ### Django checks mysqlclient version. -version_info = (1, 4, 3, "final", 0) -__version__ = "1.4.3" +version_info = (1, 4, 6, "final", 1) +__version__ = "1.4.6" def get_client_info(): # for MySQLdb compatibility From dbf1ff52a695278cd80e179641f67bb6e2a83326 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Mon, 26 Jun 2023 14:33:12 +0900 Subject: [PATCH 33/80] Fix dynamic version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 18714779..15df9f3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ include = ["pymysql*"] exclude = ["tests*", "pymysql.tests*"] [tool.setuptools.dynamic] -version = {attr = "pymysql.VERSION"} +version = {attr = "pymysql.VERSION_STRING"} [tool.ruff] line-length = 99 From 6b10225c94087d47782049aafc8e12efa512337b Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Sat, 1 Jul 2023 01:29:58 +0900 Subject: [PATCH 34/80] Disable renovate dashboard --- renovate.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/renovate.json b/renovate.json index 39a2b6e9..09e16da6 100644 --- a/renovate.json +++ b/renovate.json @@ -2,5 +2,6 @@ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:base" - ] + ], + "dependencyDashboard": false } From 8157da51e844f619eb693c5f5dd2758dca1d1c98 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 4 Sep 2023 02:04:26 -0600 Subject: [PATCH 35/80] Add support for Python 3.12 (#1134) --- .github/workflows/test.yaml | 7 +++++++ pyproject.toml | 1 + 2 files changed, 8 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6b1e0f32..1153b9e4 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -8,6 +8,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: test: runs-on: ubuntu-latest @@ -24,6 +27,9 @@ jobs: - db: "mariadb:10.6" py: "3.11" + - db: "mariadb:10.6" + py: "3.12" + - db: "mariadb:lts" py: "3.9" @@ -62,6 +68,7 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.py }} + allow-prereleases: true cache: 'pip' cache-dependency-path: 'requirements-dev.txt' diff --git a/pyproject.toml b/pyproject.toml index 15df9f3c..8e75058c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Intended Audience :: Developers", From 9e956ad5212f533a0541ee4f5e9e676d8a11b6d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 11:59:37 +0900 Subject: [PATCH 36/80] update actions/checkout action to v4 (#1136) --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/django.yaml | 2 +- .github/workflows/lint.yaml | 2 +- .github/workflows/test.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a4c434c5..13519f18 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -30,7 +30,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/django.yaml b/.github/workflows/django.yaml index da664f85..395c64fd 100644 --- a/.github/workflows/django.yaml +++ b/.github/workflows/django.yaml @@ -36,7 +36,7 @@ jobs: 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;" - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 77edb0c3..c0c013b0 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -13,7 +13,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: psf/black@stable with: diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1153b9e4..dcd1abea 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -56,7 +56,7 @@ jobs: - /run/mysqld:/run/mysqld steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Workaround MySQL container permissions if: startsWith(matrix.db, 'mysql') From c1d8063759a4a3968b0f7907e098554d9a8ad552 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 15 Sep 2023 06:55:02 +0900 Subject: [PATCH 37/80] Update codecov/codecov-action action to v4 (#1137) --- .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 dcd1abea..b28b63bd 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -106,4 +106,4 @@ jobs: - name: Upload coverage reports to Codecov if: github.repository == 'PyMySQL/PyMySQL' - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 From 39bf50e057332d27b8ccf27f11d04467fa1e3904 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 14 Nov 2023 18:34:43 +0900 Subject: [PATCH 38/80] ci: use codecov@v3 (#1142) v4 is still beta. --- .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 b28b63bd..dcd1abea 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -106,4 +106,4 @@ jobs: - name: Upload coverage reports to Codecov if: github.repository == 'PyMySQL/PyMySQL' - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v3 From 5820fa09844276477b3f6299341f9dc05d415526 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 14 Nov 2023 18:35:17 +0900 Subject: [PATCH 39/80] update CHANGELOG Add future changes. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6283670..f371ef32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ * Error classes in Cursor class will be removed after 2024-06 * `Connection.set_charset(charset)` will be removed after 2024-06 * `db` and `passwd` will emit DeprecationWarning in v1.2. See #933. - +* `Connection.ping(reconnect)` change the default to not reconnect. ## v1.1.0 From 8b514a4bc103852c8031fd4e0e634ae3d2c10c22 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Nov 2023 02:44:13 +0900 Subject: [PATCH 40/80] ci: update dessant/lock-threads action to v5 (#1141) --- .github/workflows/lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 780dd92d..21449e3b 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -13,5 +13,5 @@ jobs: if: github.repository == 'PyMySQL/PyMySQL' runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v4 + - uses: dessant/lock-threads@v5 From 523f0949f33f481e4d41c920c2e1314faeae28ab Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 15 Nov 2023 13:51:06 +0900 Subject: [PATCH 41/80] update document settings --- .readthedocs.yaml | 22 ++++ docs/Makefile | 24 ----- docs/make.bat | 242 ------------------------------------------ docs/source/conf.py | 3 +- docs/source/index.rst | 4 +- 5 files changed, 26 insertions(+), 269 deletions(-) create mode 100644 .readthedocs.yaml delete mode 100644 docs/make.bat diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..0ff55962 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,22 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/source/conf.py + +# We recommend specifying your dependencies to enable reproducible builds: +# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +# python: +# install: +# - requirements: docs/requirements.txt diff --git a/docs/Makefile b/docs/Makefile index d3725552..c1240d2b 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -74,30 +74,6 @@ json: @echo @echo "Build finished; now you can process the JSON files." -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PyMySQL.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PyMySQL.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/PyMySQL" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PyMySQL" - @echo "# devhelp" - epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index dcd4287c..00000000 --- a/docs/make.bat +++ /dev/null @@ -1,242 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source -set I18NSPHINXOPTS=%SPHINXOPTS% source -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PyMySQL.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PyMySQL.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end diff --git a/docs/source/conf.py b/docs/source/conf.py index a57a03c4..d346fbda 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -101,7 +101,8 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = "default" +# html_theme = "default" +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/docs/source/index.rst b/docs/source/index.rst index 97633f1a..e64b6423 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,5 +1,5 @@ -Welcome to PyMySQL's documentation! -=================================== +PyMySQL documentation +===================== .. toctree:: :maxdepth: 2 From f62893a2c284468091efc95e5b744abcf274dc34 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 15 Nov 2023 13:57:25 +0900 Subject: [PATCH 42/80] update document settings --- docs/source/conf.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index d346fbda..410e9c74 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -30,7 +30,7 @@ # ones. extensions = [ "sphinx.ext.autodoc", - "sphinx.ext.intersphinx", + "sphinx_rtd_theme", ] # Add any paths that contain templates here, relative to this directory. @@ -47,7 +47,7 @@ # General information about the project. project = "PyMySQL" -copyright = "2016, Yutaka Matsubara and GitHub contributors" +copyright = "2023, Inada Naoki and GitHub contributors" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -104,6 +104,7 @@ # html_theme = "default" html_theme = "sphinx_rtd_theme" + # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. From eb0b6e3429fcd1971be56cae32ffe5780c1c9cb6 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 15 Nov 2023 13:57:25 +0900 Subject: [PATCH 43/80] update document settings --- docs/source/conf.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 410e9c74..1eafbda8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -30,7 +30,6 @@ # ones. extensions = [ "sphinx.ext.autodoc", - "sphinx_rtd_theme", ] # Add any paths that contain templates here, relative to this directory. @@ -101,9 +100,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -# html_theme = "default" -html_theme = "sphinx_rtd_theme" - + html_theme = "default" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the From 7f96f9335181c5ae4992a097540348a2ae174cc6 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 15 Nov 2023 14:12:24 +0900 Subject: [PATCH 44/80] fix conf.py --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 1eafbda8..a8bee6c6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -100,7 +100,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. - html_theme = "default" +html_theme = "default" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the From 910e5fc1e2bec0e80f75ac5c2d955686e3a5242c Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 15 Nov 2023 14:28:25 +0900 Subject: [PATCH 45/80] update document settings --- .readthedocs.yaml | 15 +++++---------- docs/requirements.txt | 2 ++ 2 files changed, 7 insertions(+), 10 deletions(-) create mode 100644 docs/requirements.txt diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 0ff55962..59fdb65d 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,22 +1,17 @@ # .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details - -# Required version: 2 -# Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: - python: "3.11" + python: "3.12" + +python: + install: + - requirements: docs/requirements.txt # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/source/conf.py - -# We recommend specifying your dependencies to enable reproducible builds: -# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html -# python: -# install: -# - requirements: docs/requirements.txt diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..8d45d0b6 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +sphinx~=7.2 +sphinx-rtd-theme~=1.3.0 From 0001c409524e4738a3e686c7faf65421281fbf4f Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 15 Nov 2023 14:34:27 +0900 Subject: [PATCH 46/80] doc: use rtd theme (#1143) --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index a8bee6c6..78dc55ca 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -100,7 +100,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = "default" +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the From 1ed7cffc0335442235ac103ed458ae38f95b33b1 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 15 Nov 2023 14:39:24 +0900 Subject: [PATCH 47/80] fix ruff error --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 8e75058c..b9a3ef54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,7 @@ line-length = 99 exclude = [ "pymysql/tests/thirdparty", ] +ignore = ["E721"] [tool.pdm.dev-dependencies] dev = [ From 84d3f93701341ba34c352663d3a5fc22af2f3d32 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 15 Nov 2023 17:24:51 +0900 Subject: [PATCH 48/80] use Ruff as formatter (#1144) --- .github/workflows/lint.yaml | 14 ++++++++------ pymysql/protocol.py | 4 ++-- pymysql/tests/test_SSCursor.py | 5 +---- pymysql/tests/test_basic.py | 6 +++--- pymysql/tests/test_cursor.py | 3 +-- pymysql/tests/test_issues.py | 5 ++--- pymysql/tests/test_nextset.py | 2 +- pymysql/tests/thirdparty/test_MySQLdb/dbapi20.py | 8 ++++---- .../test_MySQLdb/test_MySQLdb_dbapi20.py | 2 +- pyproject.toml | 1 - 10 files changed, 23 insertions(+), 27 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index c0c013b0..269211c2 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -13,11 +13,13 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: checkout + uses: actions/checkout@v4 - - uses: psf/black@stable - with: - options: "--check --verbose" - src: "." + - name: lint + uses: chartboost/ruff-action@v1 - - uses: chartboost/ruff-action@v1 + - name: check format + uses: chartboost/ruff-action@v1 + with: + args: "format --diff" diff --git a/pymysql/protocol.py b/pymysql/protocol.py index 2db92d39..340d9cf2 100644 --- a/pymysql/protocol.py +++ b/pymysql/protocol.py @@ -89,8 +89,8 @@ def advance(self, length): new_position = self._position + length if new_position < 0 or new_position > len(self._data): raise Exception( - "Invalid advance amount (%s) for cursor. " - "Position=%s" % (length, new_position) + "Invalid advance amount (%s) for cursor. Position=%s" + % (length, new_position) ) self._position = new_position diff --git a/pymysql/tests/test_SSCursor.py b/pymysql/tests/test_SSCursor.py index 9cb5bafe..d5e6e2bc 100644 --- a/pymysql/tests/test_SSCursor.py +++ b/pymysql/tests/test_SSCursor.py @@ -27,10 +27,7 @@ def test_SSCursor(self): # Create table cursor.execute( - "CREATE TABLE tz_data (" - "region VARCHAR(64)," - "zone VARCHAR(64)," - "name VARCHAR(64))" + "CREATE TABLE tz_data (region VARCHAR(64), zone VARCHAR(64), name VARCHAR(64))" ) conn.begin() diff --git a/pymysql/tests/test_basic.py b/pymysql/tests/test_basic.py index e77605fd..c60b0cca 100644 --- a/pymysql/tests/test_basic.py +++ b/pymysql/tests/test_basic.py @@ -364,7 +364,7 @@ def test_bulk_insert(self): data = [(0, "bob", 21, 123), (1, "jim", 56, 45), (2, "fred", 100, 180)] cursor.executemany( - "insert into bulkinsert (id, name, age, height) " "values (%s,%s,%s,%s)", + "insert into bulkinsert (id, name, age, height) values (%s,%s,%s,%s)", data, ) self.assertEqual( @@ -414,14 +414,14 @@ def test_bulk_insert_single_record(self): cursor = conn.cursor() data = [(0, "bob", 21, 123)] cursor.executemany( - "insert into bulkinsert (id, name, age, height) " "values (%s,%s,%s,%s)", + "insert into bulkinsert (id, name, age, height) values (%s,%s,%s,%s)", data, ) cursor.execute("commit") self._verify_records(data) def test_issue_288(self): - """executemany should work with "insert ... on update" """ + """executemany should work with "insert ... on update""" conn = self.connect() cursor = conn.cursor() data = [(0, "bob", 21, 123), (1, "jim", 56, 45), (2, "fred", 100, 180)] diff --git a/pymysql/tests/test_cursor.py b/pymysql/tests/test_cursor.py index b292c206..2e267fb6 100644 --- a/pymysql/tests/test_cursor.py +++ b/pymysql/tests/test_cursor.py @@ -17,8 +17,7 @@ def setUp(self): ) cursor = conn.cursor() cursor.execute( - "insert into test (data) values " - "('row1'), ('row2'), ('row3'), ('row4'), ('row5')" + "insert into test (data) values ('row1'), ('row2'), ('row3'), ('row4'), ('row5')" ) conn.commit() cursor.close() diff --git a/pymysql/tests/test_issues.py b/pymysql/tests/test_issues.py index 3564d3a6..f1fe8dd4 100644 --- a/pymysql/tests/test_issues.py +++ b/pymysql/tests/test_issues.py @@ -401,10 +401,9 @@ def test_issue_321(self): sql_insert = "insert into issue321 (value_1, value_2) values (%s, %s)" sql_dict_insert = ( - "insert into issue321 (value_1, value_2) " - "values (%(value_1)s, %(value_2)s)" + "insert into issue321 (value_1, value_2) values (%(value_1)s, %(value_2)s)" ) - sql_select = "select * from issue321 where " "value_1 in %s and value_2=%s" + sql_select = "select * from issue321 where value_1 in %s and value_2=%s" data = [ [("a",), "\u0430"], [["b"], "\u0430"], diff --git a/pymysql/tests/test_nextset.py b/pymysql/tests/test_nextset.py index 4b6b2a77..a10f8d5b 100644 --- a/pymysql/tests/test_nextset.py +++ b/pymysql/tests/test_nextset.py @@ -75,7 +75,7 @@ def test_multi_statement_warnings(self): cursor = con.cursor() try: - cursor.execute("DROP TABLE IF EXISTS a; " "DROP TABLE IF EXISTS b;") + cursor.execute("DROP TABLE IF EXISTS a; DROP TABLE IF EXISTS b;") except TypeError: self.fail() diff --git a/pymysql/tests/thirdparty/test_MySQLdb/dbapi20.py b/pymysql/tests/thirdparty/test_MySQLdb/dbapi20.py index 83851295..fff14b86 100644 --- a/pymysql/tests/thirdparty/test_MySQLdb/dbapi20.py +++ b/pymysql/tests/thirdparty/test_MySQLdb/dbapi20.py @@ -299,7 +299,7 @@ def test_rowcount(self): self.assertEqual( cur.rowcount, -1, - "cursor.rowcount should be -1 after executing no-result " "statements", + "cursor.rowcount should be -1 after executing no-result statements", ) cur.execute( "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix) @@ -409,12 +409,12 @@ def _paraminsert(self, cur): self.assertEqual( beers[0], "Cooper's", - "cursor.fetchall retrieved incorrect data, or data inserted " "incorrectly", + "cursor.fetchall retrieved incorrect data, or data inserted incorrectly", ) self.assertEqual( beers[1], "Victoria Bitter", - "cursor.fetchall retrieved incorrect data, or data inserted " "incorrectly", + "cursor.fetchall retrieved incorrect data, or data inserted incorrectly", ) def test_executemany(self): @@ -482,7 +482,7 @@ def test_fetchone(self): self.assertEqual( cur.fetchone(), None, - "cursor.fetchone should return None if a query retrieves " "no rows", + "cursor.fetchone should return None if a query retrieves no rows", ) self.assertTrue(cur.rowcount in (-1, 0)) diff --git a/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py b/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py index c68289fe..5c34d40d 100644 --- a/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py +++ b/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py @@ -98,7 +98,7 @@ def test_fetchone(self): self.assertEqual( cur.fetchone(), None, - "cursor.fetchone should return None if a query retrieves " "no rows", + "cursor.fetchone should return None if a query retrieves no rows", ) self.assertTrue(cur.rowcount in (-1, 0)) diff --git a/pyproject.toml b/pyproject.toml index b9a3ef54..1c10b4b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,6 @@ exclude = ["tests*", "pymysql.tests*"] version = {attr = "pymysql.VERSION_STRING"} [tool.ruff] -line-length = 99 exclude = [ "pymysql/tests/thirdparty", ] From d206182822d61650eed8ccd167171ea6131113d1 Mon Sep 17 00:00:00 2001 From: Sergei Vaskov Date: Thu, 16 Nov 2023 12:39:02 +0200 Subject: [PATCH 49/80] Add ssl_key_password param (#1145) Add support for SSL private key password in Connection class to handle encrypted keys. Co-authored-by: Sergei Vaskov --- pymysql/connections.py | 10 +++- pymysql/tests/test_connection.py | 81 +++++++++++++++++++++++++++++--- 2 files changed, 82 insertions(+), 9 deletions(-) diff --git a/pymysql/connections.py b/pymysql/connections.py index 843bea5e..7e12e169 100644 --- a/pymysql/connections.py +++ b/pymysql/connections.py @@ -135,6 +135,7 @@ class Connection: :param ssl_disabled: A boolean value that disables usage of TLS. :param ssl_key: Path to the file that contains a PEM-formatted private key for the client certificate. + :param ssl_key_password: The password for the client certificate private key. :param ssl_verify_cert: Set to true to check the server certificate's validity. :param ssl_verify_identity: Set to true to check the server's identity. :param read_default_group: Group to read from in the configuration file. @@ -201,6 +202,7 @@ def __init__( ssl_cert=None, ssl_disabled=None, ssl_key=None, + ssl_key_password=None, ssl_verify_cert=None, ssl_verify_identity=None, compress=None, # not supported @@ -262,7 +264,7 @@ def _config(key, arg): if not ssl: ssl = {} if isinstance(ssl, dict): - for key in ["ca", "capath", "cert", "key", "cipher"]: + for key in ["ca", "capath", "cert", "key", "password", "cipher"]: value = _config("ssl-" + key, ssl.get(key)) if value: ssl[key] = value @@ -281,6 +283,8 @@ def _config(key, arg): ssl["cert"] = ssl_cert if ssl_key is not None: ssl["key"] = ssl_key + if ssl_key_password is not None: + ssl["password"] = ssl_key_password if ssl: if not SSL_ENABLED: raise NotImplementedError("ssl module not found") @@ -389,7 +393,9 @@ def _create_ssl_ctx(self, sslp): else: ctx.verify_mode = ssl.CERT_NONE if hasnoca else ssl.CERT_REQUIRED if "cert" in sslp: - ctx.load_cert_chain(sslp["cert"], keyfile=sslp.get("key")) + ctx.load_cert_chain( + sslp["cert"], keyfile=sslp.get("key"), password=sslp.get("password") + ) if "cipher" in sslp: ctx.set_ciphers(sslp["cipher"]) ctx.options |= ssl.OP_NO_SSLv2 diff --git a/pymysql/tests/test_connection.py b/pymysql/tests/test_connection.py index 0803efc9..ccfc4a32 100644 --- a/pymysql/tests/test_connection.py +++ b/pymysql/tests/test_connection.py @@ -574,7 +574,11 @@ def test_ssl_connect(self): assert create_default_context.called assert dummy_ssl_context.check_hostname assert dummy_ssl_context.verify_mode == ssl.CERT_REQUIRED - dummy_ssl_context.load_cert_chain.assert_called_with("cert", keyfile="key") + dummy_ssl_context.load_cert_chain.assert_called_with( + "cert", + keyfile="key", + password=None, + ) dummy_ssl_context.set_ciphers.assert_called_with("cipher") dummy_ssl_context = mock.Mock(options=0) @@ -592,7 +596,34 @@ def test_ssl_connect(self): assert create_default_context.called assert dummy_ssl_context.check_hostname assert dummy_ssl_context.verify_mode == ssl.CERT_REQUIRED - dummy_ssl_context.load_cert_chain.assert_called_with("cert", keyfile="key") + dummy_ssl_context.load_cert_chain.assert_called_with( + "cert", + keyfile="key", + password=None, + ) + dummy_ssl_context.set_ciphers.assert_not_called + + dummy_ssl_context = mock.Mock(options=0) + with mock.patch("pymysql.connections.Connection.connect"), 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", + "cert": "cert", + "key": "key", + "password": "password", + }, + ) + assert create_default_context.called + assert dummy_ssl_context.check_hostname + assert dummy_ssl_context.verify_mode == ssl.CERT_REQUIRED + dummy_ssl_context.load_cert_chain.assert_called_with( + "cert", + keyfile="key", + password="password", + ) dummy_ssl_context.set_ciphers.assert_not_called dummy_ssl_context = mock.Mock(options=0) @@ -622,7 +653,11 @@ def test_ssl_connect(self): assert create_default_context.called assert not dummy_ssl_context.check_hostname assert dummy_ssl_context.verify_mode == ssl.CERT_NONE - dummy_ssl_context.load_cert_chain.assert_called_with("cert", keyfile="key") + dummy_ssl_context.load_cert_chain.assert_called_with( + "cert", + keyfile="key", + password=None, + ) dummy_ssl_context.set_ciphers.assert_not_called for ssl_verify_cert in (True, "1", "yes", "true"): @@ -640,7 +675,9 @@ def test_ssl_connect(self): assert not dummy_ssl_context.check_hostname assert dummy_ssl_context.verify_mode == ssl.CERT_REQUIRED dummy_ssl_context.load_cert_chain.assert_called_with( - "cert", keyfile="key" + "cert", + keyfile="key", + password=None, ) dummy_ssl_context.set_ciphers.assert_not_called @@ -659,7 +696,9 @@ def test_ssl_connect(self): assert not dummy_ssl_context.check_hostname assert dummy_ssl_context.verify_mode == ssl.CERT_NONE dummy_ssl_context.load_cert_chain.assert_called_with( - "cert", keyfile="key" + "cert", + keyfile="key", + password=None, ) dummy_ssl_context.set_ciphers.assert_not_called @@ -682,7 +721,9 @@ def test_ssl_connect(self): ssl.CERT_REQUIRED if ssl_ca is not None else ssl.CERT_NONE ), (ssl_ca, ssl_verify_cert) dummy_ssl_context.load_cert_chain.assert_called_with( - "cert", keyfile="key" + "cert", + keyfile="key", + password=None, ) dummy_ssl_context.set_ciphers.assert_not_called @@ -700,7 +741,33 @@ def test_ssl_connect(self): assert create_default_context.called assert dummy_ssl_context.check_hostname assert dummy_ssl_context.verify_mode == ssl.CERT_NONE - dummy_ssl_context.load_cert_chain.assert_called_with("cert", keyfile="key") + dummy_ssl_context.load_cert_chain.assert_called_with( + "cert", + keyfile="key", + password=None, + ) + dummy_ssl_context.set_ciphers.assert_not_called + + dummy_ssl_context = mock.Mock(options=0) + with mock.patch("pymysql.connections.Connection.connect"), 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", + ssl_cert="cert", + ssl_key="key", + ssl_key_password="password", + ssl_verify_identity=True, + ) + assert create_default_context.called + assert dummy_ssl_context.check_hostname + assert dummy_ssl_context.verify_mode == ssl.CERT_NONE + dummy_ssl_context.load_cert_chain.assert_called_with( + "cert", + keyfile="key", + password="password", + ) dummy_ssl_context.set_ciphers.assert_not_called dummy_ssl_context = mock.Mock(options=0) From f476773eca5480c75e6d418abd7e73ae6c51ac22 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 13:31:03 +0900 Subject: [PATCH 50/80] chore(deps): update dependency sphinx-rtd-theme to v2 (#1147) --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 8d45d0b6..01406623 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,2 @@ sphinx~=7.2 -sphinx-rtd-theme~=1.3.0 +sphinx-rtd-theme~=2.0.0 From f13f054abcc18b39855a760a84be0a517f0da658 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 6 Dec 2023 22:34:08 +0900 Subject: [PATCH 51/80] chore(deps): update actions/setup-python action to v5 (#1152) --- .github/workflows/django.yaml | 2 +- .github/workflows/test.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/django.yaml b/.github/workflows/django.yaml index 395c64fd..5c460954 100644 --- a/.github/workflows/django.yaml +++ b/.github/workflows/django.yaml @@ -39,7 +39,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index dcd1abea..bfe8fff1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -65,7 +65,7 @@ jobs: /usr/bin/docker ps --all --filter status=exited --no-trunc --format "{{.ID}}" | xargs -r /usr/bin/docker start - name: Set up Python ${{ matrix.py }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.py }} allow-prereleases: true From 1e28be81c24dde66f8acbf4c5e24f60d6b5e72e7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 16:04:49 +0900 Subject: [PATCH 52/80] chore(deps): update github/codeql-action action to v3 (#1154) --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 13519f18..df49979e 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,7 +34,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + 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. @@ -45,7 +45,7 @@ jobs: # 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@v2 + uses: github/codeql-action/autobuild@v3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -59,4 +59,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From 1f0b7856de4008e7e4c1e8c1b215d5d4dfaecd1a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 Feb 2024 12:55:12 +0900 Subject: [PATCH 53/80] chore(deps): update codecov/codecov-action action to v4 (#1158) --- .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 bfe8fff1..6d59d8c4 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -106,4 +106,4 @@ jobs: - name: Upload coverage reports to Codecov if: github.repository == 'PyMySQL/PyMySQL' - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 From 9694747ae619e88b792a8e0b4c08036572452584 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Fri, 2 Feb 2024 15:42:24 +0900 Subject: [PATCH 54/80] pyupgrade --- docs/source/conf.py | 2 -- pymysql/connections.py | 23 +++++++++-------------- pymysql/protocol.py | 6 ++---- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 78dc55ca..158d0d12 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # PyMySQL documentation build configuration file, created by # sphinx-quickstart on Tue May 17 12:01:11 2016. # diff --git a/pymysql/connections.py b/pymysql/connections.py index 7e12e169..dc121e1b 100644 --- a/pymysql/connections.py +++ b/pymysql/connections.py @@ -84,8 +84,7 @@ def _lenenc_int(i): return b"\xfe" + struct.pack(" len(self._data): raise Exception( - "Invalid advance amount (%s) for cursor. Position=%s" - % (length, new_position) + f"Invalid advance amount ({length}) for cursor. Position={new_position}" ) self._position = new_position From bbd049f40db9c696574ce6f31669880042c56d79 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Fri, 2 Feb 2024 17:16:41 +0900 Subject: [PATCH 55/80] Support error packet without sqlstate (#1160) Fix #1156 --- pymysql/connections.py | 2 -- pymysql/err.py | 9 ++++++++- pymysql/tests/test_err.py | 22 ++++++++++++---------- pyproject.toml | 2 ++ 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/pymysql/connections.py b/pymysql/connections.py index dc121e1b..3a04ddd6 100644 --- a/pymysql/connections.py +++ b/pymysql/connections.py @@ -765,8 +765,6 @@ def _read_packet(self, packet_type=MysqlPacket): dump_packet(recv_data) buff += recv_data # https://dev.mysql.com/doc/internals/en/sending-more-than-16mbyte.html - if bytes_to_read == 0xFFFFFF: - continue if bytes_to_read < MAX_PACKET_LEN: break diff --git a/pymysql/err.py b/pymysql/err.py index 3da5b166..dac65d3b 100644 --- a/pymysql/err.py +++ b/pymysql/err.py @@ -136,7 +136,14 @@ def _map_error(exc, *errors): def raise_mysql_exception(data): errno = struct.unpack(" Date: Tue, 26 Mar 2024 18:02:41 +1100 Subject: [PATCH 56/80] test json - mariadb without JSON type (#1165) MariaDB-11.0.1 removed the 5.5.5 version hack (MDEV-28910). MariaDB still doesn't support JSON as a type. Use get_mysql_vendor() == mysql for the final part of test_json. --- pymysql/tests/test_basic.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pymysql/tests/test_basic.py b/pymysql/tests/test_basic.py index c60b0cca..0fe13b59 100644 --- a/pymysql/tests/test_basic.py +++ b/pymysql/tests/test_basic.py @@ -323,9 +323,10 @@ def test_json(self): res = cur.fetchone()[0] self.assertEqual(json.loads(res), json.loads(json_str)) - cur.execute("SELECT CAST(%s AS JSON) AS x", (json_str,)) - res = cur.fetchone()[0] - self.assertEqual(json.loads(res), json.loads(json_str)) + if self.get_mysql_vendor(conn) == "mysql": + cur.execute("SELECT CAST(%s AS JSON) AS x", (json_str,)) + res = cur.fetchone()[0] + self.assertEqual(json.loads(res), json.loads(json_str)) class TestBulkInserts(base.PyMySQLTestCase): From 69f6c7439bee14784e0ea70ae107af6446cc0c67 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Sat, 4 May 2024 15:55:22 +0900 Subject: [PATCH 57/80] ruff format --- pymysql/__init__.py | 1 + pymysql/_auth.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pymysql/__init__.py b/pymysql/__init__.py index 53625d37..37395551 100644 --- a/pymysql/__init__.py +++ b/pymysql/__init__.py @@ -21,6 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + import sys from .constants import FIELD_TYPE diff --git a/pymysql/_auth.py b/pymysql/_auth.py index 99987b77..8ce744fb 100644 --- a/pymysql/_auth.py +++ b/pymysql/_auth.py @@ -1,6 +1,7 @@ """ Implements auth methods """ + from .err import OperationalError From 7f032a699d55340f05101deb4d7d4f63db4adc11 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Mon, 20 May 2024 13:25:18 +0900 Subject: [PATCH 58/80] remove coveralls from requirements --- requirements-dev.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 13d7f7fb..140d3706 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,4 +2,3 @@ cryptography PyNaCl>=1.4.0 pytest pytest-cov -coveralls From 521e40050cb386a499f68f483fefd144c493053c Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Sat, 18 May 2024 11:33:30 +0900 Subject: [PATCH 59/80] forbid dict parameter --- pymysql/converters.py | 6 +----- pymysql/tests/test_connection.py | 7 +++++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pymysql/converters.py b/pymysql/converters.py index 1adac752..dbf97ca7 100644 --- a/pymysql/converters.py +++ b/pymysql/converters.py @@ -27,11 +27,7 @@ def escape_item(val, charset, mapping=None): def escape_dict(val, charset, mapping=None): - n = {} - for k, v in val.items(): - quoted = escape_item(v, charset, mapping) - n[k] = quoted - return n + raise TypeError("dict can not be used as parameter") def escape_sequence(val, charset, mapping=None): diff --git a/pymysql/tests/test_connection.py b/pymysql/tests/test_connection.py index ccfc4a32..dcf3394c 100644 --- a/pymysql/tests/test_connection.py +++ b/pymysql/tests/test_connection.py @@ -848,12 +848,15 @@ def test_escape_no_default(self): self.assertRaises(TypeError, con.escape, 42, {}) - def test_escape_dict_value(self): + def test_escape_dict_raise_typeerror(self): + """con.escape(dict) should raise TypeError""" con = self.connect() 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()}) def test_escape_list_item(self): con = self.connect() From 2cab9ecc641e962565c6254a5091f90c47f59b35 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 21 May 2024 20:01:22 +0900 Subject: [PATCH 60/80] v1.1.1 --- CHANGELOG.md | 15 +++++++++++++++ pymysql/__init__.py | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f371ef32..825dc47c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ * `db` and `passwd` will emit DeprecationWarning in v1.2. See #933. * `Connection.ping(reconnect)` change the default to not reconnect. +## v1.1.1 + +Release date: 2024-05-21 + +> [!WARNING] +> This release fixes a vulnerability (CVE-2024-36039). +> All users are recommended to update to this version. +> +> If you can not update soon, check the input value from +> untrusted source has an expected type. Only dict input +> from untrusted source can be an attack vector. + +* Prohibit dict parameter for `Cursor.execute()`. It didn't produce valid SQL + and might cause SQL injection. (CVE-2024-36039) + ## v1.1.0 Release date: 2023-06-26 diff --git a/pymysql/__init__.py b/pymysql/__init__.py index 37395551..bbf9023e 100644 --- a/pymysql/__init__.py +++ b/pymysql/__init__.py @@ -49,8 +49,8 @@ # PyMySQL version. # Used by setuptools and connection_attrs -VERSION = (1, 1, 0, "final", 1) -VERSION_STRING = "1.1.0" +VERSION = (1, 1, 1, "final", 1) +VERSION_STRING = "1.1.1" ### for mysqlclient compatibility ### Django checks mysqlclient version. From a6ae2c71966fb65b071c9066e21d8c806df42f15 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 21 May 2024 20:04:54 +0900 Subject: [PATCH 61/80] 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 62/80] 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 63/80] 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 64/80] 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 65/80] 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 66/80] 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 67/80] 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 68/80] 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 69/80] 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 70/80] 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 71/80] 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 72/80] 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 73/80] 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 74/80] 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 75/80] 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 76/80] 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 77/80] 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 78/80] 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 79/80] 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 80/80] 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