From 39557ab7f5eb8d075b64d78b74a17ae8d6431fcc Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Mon, 9 Nov 2026 05:39:43 -0500 Subject: [PATCH] update pymysql dependency to v1.2.4 switching to some constants which had been upstreamed. This contains a fairly important security fix: * https://github.com/PyMySQL/PyMySQL/blob/main/CHANGELOG.md#v124-security-fix --- changelog.md | 1 + mycli/client_connection.py | 5 ++--- mycli/constants.py | 4 ---- mycli/main_modes/repl.py | 4 ++-- mycli/packages/execution/sql_execute.py | 8 ++++---- mycli_test/pytests/test_client_connection.py | 5 +++-- mycli_test/pytests/test_main.py | 4 ++-- mycli_test/pytests/test_main_modes_repl.py | 2 +- mycli_test/pytests/test_sql_execute.py | 2 +- pyproject.toml | 3 ++- 10 files changed, 18 insertions(+), 20 deletions(-) diff --git a/changelog.md b/changelog.md index 3408624d1..bfb48707a 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,7 @@ Internal * Improve test coverage for `packages/special_commands/io_commands.py`. * Improve test coverage for `packages/execution/sql_execute.py`. * Improve test coverage for `packages/tabular_output/sql_format.py`. +* Update `pymysql` dependency to v1.2.4. v2.30.3 (2026/11/05) diff --git a/mycli/client_connection.py b/mycli/client_connection.py index 344f1e1ce..1ed56838f 100644 --- a/mycli/client_connection.py +++ b/mycli/client_connection.py @@ -10,7 +10,7 @@ import keyring import pymysql from pymysql.constants.CR import CR_SERVER_LOST -from pymysql.constants.ER import ACCESS_DENIED_ERROR, HANDSHAKE_ERROR +from pymysql.constants.ER import ACCESS_DENIED_ERROR, HANDSHAKE_ERROR, MUST_CHANGE_PASSWORD_LOGIN from mycli.compat import WIN from mycli.config import str_to_bool @@ -19,7 +19,6 @@ DEFAULT_HOST, DEFAULT_PORT, EMPTY_PASSWORD_FLAG_SENTINEL, - ER_MUST_CHANGE_PASSWORD_LOGIN, ) from mycli.packages.credentials.password_sources import PasswordCandidates from mycli.packages.execution.sql_execute import SQLExecute @@ -433,7 +432,7 @@ def _connect( keyring_retrieved_cleanly=keyring_retrieved_cleanly, keyring_save_eligible=keyring_save_eligible, ) - elif e1.args[0] == ER_MUST_CHANGE_PASSWORD_LOGIN: + elif e1.args[0] == MUST_CHANGE_PASSWORD_LOGIN: self.echo( "Your password has expired and the server rejected the connection.", err=True, diff --git a/mycli/constants.py b/mycli/constants.py index b80fc68a3..696da41c2 100644 --- a/mycli/constants.py +++ b/mycli/constants.py @@ -38,10 +38,6 @@ DEFAULT_WIDTH = 80 DEFAULT_HEIGHT = 25 -# MySQL error codes not available in pymysql.constants.ER -ER_MUST_CHANGE_PASSWORD_LOGIN = 1862 -ER_MUST_CHANGE_PASSWORD = 1820 - EMPTY_PASSWORD_FLAG_SENTINEL = -1 DEFAULT_PROMPT = "\\t \\u@\\h:\\d> " diff --git a/mycli/main_modes/repl.py b/mycli/main_modes/repl.py index 7bbf4164a..4579fcd2e 100644 --- a/mycli/main_modes/repl.py +++ b/mycli/main_modes/repl.py @@ -49,6 +49,7 @@ CR_SERVER_GONE_ERROR, CR_SERVER_LOST, ) +from pymysql.constants.ER import MUST_CHANGE_PASSWORD from pymysql.constants.SERVER_STATUS import SERVER_STATUS_IN_TRANS from pymysql.cursors import Cursor @@ -57,7 +58,6 @@ from mycli.constants import ( DEFAULT_HOST, DEFAULT_WIDTH, - ER_MUST_CHANGE_PASSWORD, HOME_URL, ISSUES_URL, QueryState, @@ -1110,7 +1110,7 @@ def _one_iteration( mycli.echo(str(exc), err=True, fg='red') except pymysql.OperationalError as e1: mycli.logger.debug('Exception: %r', e1) - if e1.args[0] == ER_MUST_CHANGE_PASSWORD: + if e1.args[0] == MUST_CHANGE_PASSWORD: mycli.sandbox_mode = True mycli.echo( "ERROR 1820: You must reset your password using ALTER USER or SET PASSWORD before executing this statement.", diff --git a/mycli/packages/execution/sql_execute.py b/mycli/packages/execution/sql_execute.py index 8013753f7..c632a7cdb 100644 --- a/mycli/packages/execution/sql_execute.py +++ b/mycli/packages/execution/sql_execute.py @@ -12,10 +12,10 @@ from pymysql.connections import Connection from pymysql.constants import FIELD_TYPE from pymysql.constants.CR import CR_SSL_CONNECTION_ERROR +from pymysql.constants.ER import MUST_CHANGE_PASSWORD from pymysql.converters import conversions, convert_date, convert_datetime, convert_time, decoders from pymysql.cursors import Cursor, SSCursor -from mycli.constants import ER_MUST_CHANGE_PASSWORD from mycli.packages.execution.background_runner import BackgroundRunner from mycli.packages.special_commands import io_commands from mycli.packages.special_commands.main import CommandNotFound, execute @@ -296,9 +296,9 @@ def connect( connect_kwargs['ssl_disabled'] = True conn = pymysql.connect(**connect_kwargs) # type: ignore[misc] except pymysql.OperationalError as e: - if e.args[0] == ER_MUST_CHANGE_PASSWORD: + if e.args[0] == MUST_CHANGE_PASSWORD: # Post-handshake queries (SET NAMES, SET AUTOCOMMIT, init_command) - # fail with ER_MUST_CHANGE_PASSWORD in sandbox mode. + # fail with MUST_CHANGE_PASSWORD in sandbox mode. # Reconnect with only the raw handshake. connect_kwargs['defer_connect'] = True connect_kwargs['autocommit'] = None @@ -659,7 +659,7 @@ def _connect_sandbox(conn: Connection) -> None: """Connect in sandbox mode, performing only the handshake. pymysql's normal connect() runs post-handshake queries (SET NAMES, - SET AUTOCOMMIT, init_command) that all fail with ER_MUST_CHANGE_PASSWORD + SET AUTOCOMMIT, init_command) that all fail with MUST_CHANGE_PASSWORD in sandbox mode. This method performs the raw socket connection and authentication handshake only. """ diff --git a/mycli_test/pytests/test_client_connection.py b/mycli_test/pytests/test_client_connection.py index 32adc05be..d385d7152 100644 --- a/mycli_test/pytests/test_client_connection.py +++ b/mycli_test/pytests/test_client_connection.py @@ -8,11 +8,12 @@ from configobj import ConfigObj import pymysql +from pymysql.constants.ER import MUST_CHANGE_PASSWORD_LOGIN import pytest from mycli import client_connection from mycli.client_connection import ClientConnectionMixin -from mycli.constants import DEFAULT_CHARSET, EMPTY_PASSWORD_FLAG_SENTINEL, ER_MUST_CHANGE_PASSWORD_LOGIN +from mycli.constants import DEFAULT_CHARSET, EMPTY_PASSWORD_FLAG_SENTINEL from mycli.packages.credentials.password_sources import KNOWN_PASSWORD_SOURCES, PasswordCandidates @@ -1304,7 +1305,7 @@ def test_connect_exits_when_password_retry_still_has_no_password(monkeypatch: py def test_connect_reports_expired_password_login_error() -> None: client = DummyClient() - FakeSQLExecute.effects = [op_error(ER_MUST_CHANGE_PASSWORD_LOGIN, 'expired')] + FakeSQLExecute.effects = [op_error(MUST_CHANGE_PASSWORD_LOGIN, 'expired')] with pytest.raises(SystemExit) as excinfo: client.connect(host='db', port=3307) diff --git a/mycli_test/pytests/test_main.py b/mycli_test/pytests/test_main.py index 3092cab34..1afcb78d2 100644 --- a/mycli_test/pytests/test_main.py +++ b/mycli_test/pytests/test_main.py @@ -25,6 +25,7 @@ to_plain_text, ) import pymysql +from pymysql.constants.ER import MUST_CHANGE_PASSWORD_LOGIN from pymysql.err import OperationalError import pytest @@ -40,7 +41,6 @@ DEFAULT_PORT, DEFAULT_USER, EMPTY_PASSWORD_FLAG_SENTINEL, - ER_MUST_CHANGE_PASSWORD_LOGIN, TEST_DATABASE, ) from mycli.main import ( @@ -2386,7 +2386,7 @@ def test_connect_reports_expired_password_login_error(monkeypatch: pytest.Monkey class ExpiredPasswordSQLExecute(RecordingSQLExecute): calls: list[dict[str, Any]] = [] - side_effects: list[Any] = [pymysql.OperationalError(ER_MUST_CHANGE_PASSWORD_LOGIN, 'must change password')] + side_effects: list[Any] = [pymysql.OperationalError(MUST_CHANGE_PASSWORD_LOGIN, 'must change password')] monkeypatch.setattr(mycli.client_connection, 'SQLExecute', ExpiredPasswordSQLExecute) diff --git a/mycli_test/pytests/test_main_modes_repl.py b/mycli_test/pytests/test_main_modes_repl.py index 76dba9922..aaf7b49a1 100644 --- a/mycli_test/pytests/test_main_modes_repl.py +++ b/mycli_test/pytests/test_main_modes_repl.py @@ -2238,7 +2238,7 @@ class FakeSQLExecute: connection_id = 0 def run(self, text: str) -> Iterator[SQLResult]: - raise pymysql.OperationalError(repl_mode.ER_MUST_CHANGE_PASSWORD, 'must change password') + raise pymysql.OperationalError(repl_mode.MUST_CHANGE_PASSWORD, 'must change password') cli = make_repl_cli(FakeSQLExecute()) diff --git a/mycli_test/pytests/test_sql_execute.py b/mycli_test/pytests/test_sql_execute.py index 0e31eaa55..8d26f5f82 100644 --- a/mycli_test/pytests/test_sql_execute.py +++ b/mycli_test/pytests/test_sql_execute.py @@ -1000,7 +1000,7 @@ def test_connect_enters_sandbox_after_ssl_fallback(monkeypatch: pytest.MonkeyPat connect = Mock( side_effect=[ pymysql.OperationalError(sql_execute.CR_SSL_CONNECTION_ERROR, 'SSL unsupported'), - pymysql.OperationalError(sql_execute.ER_MUST_CHANGE_PASSWORD, 'must change password'), + pymysql.OperationalError(sql_execute.MUST_CHANGE_PASSWORD, 'must change password'), new_conn, ] ) diff --git a/pyproject.toml b/pyproject.toml index aeef341be..c362fea2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "cryptography ~= 50.0.2", "Pygments ~= 2.21.0", "prompt_toolkit ~= 3.0.53", - "PyMySQL ~= 1.2.3", + "PyMySQL ~= 1.2.4", "sqlparse ~= 0.6.0", "sqlglot ~= 30.25.0", "sqlglotc ~= 30.25.0", @@ -101,6 +101,7 @@ exclude-newer = '1 week' [tool.uv.exclude-newer-package] cli_helpers = false +pymysql = false # removeme 2026-11-16 [tool.ruff] target-version = 'py311'