Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-PyMySQL for openSUSE:Factory checked in at 2026-05-26 16:34:14 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-PyMySQL (Old) and /work/SRC/openSUSE:Factory/.python-PyMySQL.new.2084 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-PyMySQL" Tue May 26 16:34:14 2026 rev:25 rq:1355103 version:1.2.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-PyMySQL/python-PyMySQL.changes 2025-09-30 17:43:23.387788270 +0200 +++ /work/SRC/openSUSE:Factory/.python-PyMySQL.new.2084/python-PyMySQL.changes 2026-05-26 16:34:21.496806957 +0200 @@ -1,0 +2,32 @@ +Mon May 25 20:05:55 UTC 2026 - Dirk Müller <[email protected]> + +- update to 1.2.0: + * `Connection.ping()` change the default to not reconnect and + deprecate `reconnect` argument. + * Create a new connection if you want to reconnect. + * `connect()` arguments `db` and `passwd` now emit + DeprecationWarning. + * Use `database` and `password` instead. + * Reorganize TLS connection behavior. + * PyMySQL uses TLS by default when server supports it. + * Use `ssl_disabled=True` to prohibit SSL. + * When `ssl_verify_cert=True`, `ssl_verify_identity=True`, an + `ssl.SSLContext` is passed, + * or when any other SSL option is configured, the connection + **requires** SSL and raises + * `OperationalError` (CR_SSL_CONNECTION_ERROR) if the server + doesn't support it. + * Support MySQL 8 row/column alias syntax in `executemany` + INSERT regex. + * Expose SQLSTATE on MySQL protocol exceptions without changing + exception formatting. + * Reject non-finite `decimal.Decimal` query parameters (`NaN`, + `sNaN`, `±Infinity`). + * `Connection.set_charset(charset)` now emits + `DeprecationWarning`. + * Fix `Cursor.callproc()` didn't escape procedure name. + * There was a possibility of SQL injection when calling a + procedure with a string received from an untrusted source as + the procedure name. + +------------------------------------------------------------------- Old: ---- PyMySQL-1.1.2.tar.gz New: ---- PyMySQL-1.2.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-PyMySQL.spec ++++++ --- /var/tmp/diff_new_pack.vlUkKT/_old 2026-05-26 16:34:22.756859088 +0200 +++ /var/tmp/diff_new_pack.vlUkKT/_new 2026-05-26 16:34:22.756859088 +0200 @@ -1,7 +1,7 @@ # # spec file for package python-PyMySQL # -# Copyright (c) 2025 SUSE LLC and contributors +# Copyright (c) 2026 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -25,7 +25,7 @@ %{?sle15_python_module_pythons} Name: python-PyMySQL -Version: 1.1.2 +Version: 1.2.0 Release: 0 Summary: Pure Python MySQL Driver License: MIT ++++++ PyMySQL-1.1.2.tar.gz -> PyMySQL-1.2.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyMySQL-1.1.2/.github/dependabot.yml new/PyMySQL-1.2.0/.github/dependabot.yml --- old/PyMySQL-1.1.2/.github/dependabot.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/PyMySQL-1.2.0/.github/dependabot.yml 2026-05-19 09:48:58.000000000 +0200 @@ -0,0 +1,15 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + all-dependencies: + patterns: + - "*" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyMySQL-1.1.2/.github/workflows/django.yaml new/PyMySQL-1.2.0/.github/workflows/django.yaml --- old/PyMySQL-1.1.2/.github/workflows/django.yaml 2025-08-24 14:53:42.000000000 +0200 +++ new/PyMySQL-1.2.0/.github/workflows/django.yaml 2026-05-19 09:48:58.000000000 +0200 @@ -36,10 +36,10 @@ 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@v4 + - uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyMySQL-1.1.2/.github/workflows/lint.yaml new/PyMySQL-1.2.0/.github/workflows/lint.yaml --- old/PyMySQL-1.1.2/.github/workflows/lint.yaml 2025-08-24 14:53:42.000000000 +0200 +++ new/PyMySQL-1.2.0/.github/workflows/lint.yaml 2026-05-19 09:48:58.000000000 +0200 @@ -11,9 +11,9 @@ jobs: lint: - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: astral-sh/ruff-action@v3 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyMySQL-1.1.2/.github/workflows/publish.yml new/PyMySQL-1.2.0/.github/workflows/publish.yml --- old/PyMySQL-1.1.2/.github/workflows/publish.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/PyMySQL-1.2.0/.github/workflows/publish.yml 2026-05-19 09:48:58.000000000 +0200 @@ -0,0 +1,96 @@ +# This file is copied from https://github.com/psf/requests/blob/8f6cda9969f4a98c45ea2922f6bcbefc02256202/.github/workflows/publish.yml +name: Publish to PyPI + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + test-pypi-only: + description: "Publish to Test PyPI only" + type: boolean + default: true + +permissions: + contents: read + +jobs: + build: + name: "Build dists" + runs-on: "ubuntu-slim" + outputs: + artifact-id: ${{ steps.upload-artifact.outputs.artifact-id }} + + steps: + - name: "Checkout repository" + uses: "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6.0.2 + with: + persist-credentials: false + + - name: "Setup Python" + uses: "actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405" # v6.2.0 + with: + python-version: "3.x" + + - name: "Install dependencies" + run: python -m pip install build==1.5.0 + + - name: "Build dists" + run: | + SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) \ + python -m build + + - name: "Upload dists" + uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" + id: upload-artifact + with: + name: "dist" + path: "dist/" + if-no-files-found: error + retention-days: 5 + + publish: + name: "Publish" + if: startsWith(github.ref, 'refs/tags/') + needs: ["build"] + permissions: + id-token: write + runs-on: "ubuntu-latest" + environment: + name: "publish" + + steps: + - name: "Download dists" + uses: "actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c" # v8.0.1 + with: + artifact-ids: ${{ needs.build.outputs.artifact-id }} + path: "dist/" + + - name: "Publish dists to PyPI" + uses: "pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b" # v1.14.0 + with: + attestations: true + + publish-test-pypi: + name: "Publish to Test PyPI" + if: github.event_name == 'workflow_dispatch' + needs: ["build"] + permissions: + id-token: write + runs-on: "ubuntu-latest" + environment: + name: "testpypi" + + steps: + - name: "Download dists" + uses: "actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c" # v8.0.1 + with: + artifact-ids: ${{ needs.build.outputs.artifact-id }} + path: "dist/" + + - name: "Publish dists to Test PyPI" + uses: "pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b" # v1.14.0 + with: + repository-url: https://test.pypi.org/legacy/ + attestations: true diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyMySQL-1.1.2/.github/workflows/test.yaml new/PyMySQL-1.2.0/.github/workflows/test.yaml --- old/PyMySQL-1.1.2/.github/workflows/test.yaml 2025-08-24 14:53:42.000000000 +0200 +++ new/PyMySQL-1.2.0/.github/workflows/test.yaml 2026-05-19 09:48:58.000000000 +0200 @@ -63,7 +63,7 @@ - /run/mysqld:/run/mysqld steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Workaround MySQL container permissions if: startsWith(matrix.db, 'mysql') @@ -72,7 +72,7 @@ /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@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.py }} allow-prereleases: true @@ -113,4 +113,4 @@ - name: Upload coverage reports to Codecov if: github.repository == 'PyMySQL/PyMySQL' - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyMySQL-1.1.2/AGENTS.md new/PyMySQL-1.2.0/AGENTS.md --- old/PyMySQL-1.1.2/AGENTS.md 1970-01-01 01:00:00.000000000 +0100 +++ new/PyMySQL-1.2.0/AGENTS.md 2026-05-19 09:48:58.000000000 +0200 @@ -0,0 +1 @@ +Do `ruff format` and `ruff check` before commit. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyMySQL-1.1.2/CHANGELOG.md new/PyMySQL-1.2.0/CHANGELOG.md --- old/PyMySQL-1.1.2/CHANGELOG.md 2025-08-24 14:53:42.000000000 +0200 +++ new/PyMySQL-1.2.0/CHANGELOG.md 2026-05-19 09:48:58.000000000 +0200 @@ -1,11 +1,46 @@ # Changes -## Backward incompatible changes planned in the future. +## v1.2.0 -* 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. +Release date: 2026-05-19 + +### Breaking changes + +* `Connection.ping()` change the default to not reconnect and deprecate `reconnect` argument. + Create a new connection if you want to reconnect. (#1241) + +* Error classes in Cursor class are removed. (#1240) + +* `connect()` arguments `db` and `passwd` now emit DeprecationWarning. + Use `database` and `password` instead. (#1240) + +* Reorganize TLS connection behavior. + + * PyMySQL uses TLS by default when server supports it. + Use `ssl_disabled=True` to prohibit SSL. (#1213) + + * When `ssl_verify_cert=True`, `ssl_verify_identity=True`, an `ssl.SSLContext` is passed, + or when any other SSL option is configured, the connection **requires** SSL and raises + `OperationalError` (CR_SSL_CONNECTION_ERROR) if the server doesn't support it. (#1234) + +### Other changes + +* Support MySQL 8 row/column alias syntax in `executemany` INSERT regex. (#1235) +* Expose SQLSTATE on MySQL protocol exceptions without changing exception formatting. (#1236) +* Reject non-finite `decimal.Decimal` query parameters (`NaN`, `sNaN`, `±Infinity`). (#1237) +* `Connection.set_charset(charset)` now emits `DeprecationWarning`. + + +## v1.1.3 + +Release date: 2026-05-01 + +### Security + +* Fix `Cursor.callproc()` didn't escape procedure name. (#1206) + There was a possibility of SQL injection when calling a procedure with a string received from an untrusted source as the procedure name. + + NOTICE: This change may cause backward compatibility issues. If you specified a procedure name like `"dbname.funcname"`, the previous version called `CALL dbname.funcname`, but from this version, it will call ``CALL `dbname.funcname` `` so you cannot specify procedure name with database name anymore. ## v1.1.2 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyMySQL-1.1.2/docs/source/conf.py new/PyMySQL-1.2.0/docs/source/conf.py --- old/PyMySQL-1.1.2/docs/source/conf.py 2025-08-24 14:53:42.000000000 +0200 +++ new/PyMySQL-1.2.0/docs/source/conf.py 2026-05-19 09:48:58.000000000 +0200 @@ -273,4 +273,4 @@ # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {"http://docs.python.org/": None} +intersphinx_mapping = {"https://docs.python.org/3": None} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyMySQL-1.1.2/docs/source/modules/index.rst new/PyMySQL-1.2.0/docs/source/modules/index.rst --- old/PyMySQL-1.1.2/docs/source/modules/index.rst 2025-08-24 14:53:42.000000000 +0200 +++ new/PyMySQL-1.2.0/docs/source/modules/index.rst 2026-05-19 09:48:58.000000000 +0200 @@ -5,7 +5,7 @@ method, this part of the documentation is for you. For more information, please read the `Python Database API specification -<https://www.python.org/dev/peps/pep-0249>`_. +<https://peps.python.org/pep-0249/>`_. .. toctree:: :maxdepth: 2 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyMySQL-1.1.2/docs/source/user/installation.rst new/PyMySQL-1.2.0/docs/source/user/installation.rst --- old/PyMySQL-1.1.2/docs/source/user/installation.rst 2025-08-24 14:53:42.000000000 +0200 +++ new/PyMySQL-1.2.0/docs/source/user/installation.rst 2026-05-19 09:48:58.000000000 +0200 @@ -18,7 +18,7 @@ * Python -- one of the following: - - CPython_ >= 3.7 + - CPython_ >= 3.9 - Latest PyPy_ 3 * MySQL Server -- one of the following: @@ -26,7 +26,7 @@ - MySQL_ >= 5.7 - MariaDB_ >= 10.3 -.. _CPython: http://www.python.org/ -.. _PyPy: http://pypy.org/ -.. _MySQL: http://www.mysql.com/ +.. _CPython: https://www.python.org/ +.. _PyPy: https://pypy.org/ +.. _MySQL: https://www.mysql.com/ .. _MariaDB: https://mariadb.org/ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyMySQL-1.1.2/docs/source/user/resources.rst new/PyMySQL-1.2.0/docs/source/user/resources.rst --- old/PyMySQL-1.1.2/docs/source/user/resources.rst 2025-08-24 14:53:42.000000000 +0200 +++ new/PyMySQL-1.2.0/docs/source/user/resources.rst 2026-05-19 09:48:58.000000000 +0200 @@ -4,11 +4,11 @@ Resources ============ -DB-API 2.0: http://www.python.org/dev/peps/pep-0249 +DB-API 2.0: https://peps.python.org/pep-0249/ -MySQL Reference Manuals: http://dev.mysql.com/doc/ +MySQL Reference Manuals: https://dev.mysql.com/doc/ MySQL client/server protocol: -http://dev.mysql.com/doc/internals/en/client-server-protocol.html +https://dev.mysql.com/doc/dev/mysql-server/latest/PAGE_PROTOCOL.html PyMySQL mailing list: https://groups.google.com/forum/#!forum/pymysql-users diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyMySQL-1.1.2/pymysql/__init__.py new/PyMySQL-1.2.0/pymysql/__init__.py --- old/PyMySQL-1.1.2/pymysql/__init__.py 2025-08-24 14:53:42.000000000 +0200 +++ new/PyMySQL-1.2.0/pymysql/__init__.py 2026-05-19 09:48:58.000000000 +0200 @@ -49,13 +49,13 @@ # PyMySQL version. # Used by setuptools and connection_attrs -VERSION = (1, 1, 2, "final") -VERSION_STRING = "1.1.2" +VERSION = (1, 2, 0, "final") +VERSION_STRING = "1.2.0" ### for mysqlclient compatibility ### Django checks mysqlclient version. -version_info = (1, 4, 6, "final", 1) -__version__ = "1.4.6" +version_info = (2, 2, 8, "final", 1) +__version__ = "2.2.8" def get_client_info(): # for MySQLdb compatibility diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyMySQL-1.1.2/pymysql/connections.py new/PyMySQL-1.2.0/pymysql/connections.py --- old/PyMySQL-1.1.2/pymysql/connections.py 2025-08-24 14:53:42.000000000 +0200 +++ new/PyMySQL-1.2.0/pymysql/connections.py 2026-05-19 09:48:58.000000000 +0200 @@ -131,10 +131,13 @@ :param init_command: Initial SQL statement to run when connection is established. :param connect_timeout: The timeout for connecting to the database in seconds. (default: 10, min: 1, max: 31536000) - :param ssl: A dict of arguments similar to mysql_ssl_set()'s parameters or an ssl.SSLContext. + :param ssl: An ssl.SSLContext, or a dict of arguments similar to mysql_ssl_set()'s parameters. + Passing a dict is deprecated; use the individual ``ssl_*`` parameters or an + ``ssl.SSLContext`` instead. :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_disabled: A boolean value that disables usage of TLS. Unlike other SSL options, + setting this to True explicitly prohibits the use of TLS, even if the server supports it. :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. @@ -214,16 +217,12 @@ db=None, # deprecated ): if db is not None and database is None: - # We will raise warning in 2022 or later. - # See https://github.com/PyMySQL/PyMySQL/issues/939 - # warnings.warn("'db' is deprecated, use 'database'", DeprecationWarning, 3) + warnings.warn("'db' is deprecated, use 'database'", DeprecationWarning, 3) database = db if passwd is not None and not password: - # We will raise warning in 2022 or later. - # See https://github.com/PyMySQL/PyMySQL/issues/939 - # warnings.warn( - # "'passwd' is deprecated, use 'password'", DeprecationWarning, 3 - # ) + warnings.warn( + "'passwd' is deprecated, use 'password'", DeprecationWarning, 3 + ) password = passwd if compress or named_pipe: @@ -273,6 +272,7 @@ ssl[key] = value self.ssl = False + self._ssl_required = False if not ssl_disabled: if ssl_ca or ssl_cert or ssl_key or ssl_verify_cert or ssl_verify_identity: ssl = { @@ -292,8 +292,15 @@ if not SSL_ENABLED: raise NotImplementedError("ssl module not found") self.ssl = True + self._ssl_required = True client_flag |= CLIENT.SSL self.ctx = self._create_ssl_ctx(ssl) + elif SSL_ENABLED: + # No explicit SSL options specified: use PREFERRED mode. + # Attempt SSL but fall back gracefully if the server doesn't support it. + self.ssl = True + self._ssl_required = False + self.ctx = self._create_ssl_ctx({}) self.host = host or "localhost" self.port = port or 3306 @@ -587,15 +594,24 @@ raise TypeError("thread_id must be an integer") self.query(f"KILL {thread_id:d}") - def ping(self, reconnect=True): + def ping(self, reconnect=False): """ Check if the server is alive. + `reconnect` is deprecated. Create a new connection if you want to reconnect. + :param reconnect: If the connection is closed, reconnect. :type reconnect: boolean :raise Error: If the connection is closed and reconnect=False. """ + # emit deprecation warning for reconnect. + if reconnect: + warnings.warn( + "The 'reconnect' argument is deprecated. Create a new connection if you want to reconnect.", + DeprecationWarning, + 2, + ) if self._sock is None: if reconnect: self.connect() @@ -614,6 +630,11 @@ def set_charset(self, charset): """Deprecated. Use set_character_set() instead.""" + warnings.warn( + "'set_charset' is deprecated, use 'set_character_set' instead", + DeprecationWarning, + 2, + ) # This function has been implemented in old PyMySQL. # But this name is different from MySQLdb. # So we keep this function for compatibility and add @@ -890,13 +911,34 @@ if isinstance(self.user, str): self.user = self.user.encode(self.encoding) + # Determine flags for the initial handshake packet. + # CLIENT.SSL is added conditionally: for REQUIRED mode it is already set in + # self.client_flag, but for PREFERRED mode it is only added when the server + # also advertises SSL support. + # _do_ssl is set here and checked below for sha256_password auth. + client_flags = self.client_flag + if self.ssl: + if self.server_capabilities & CLIENT.SSL: + # SSL upgrade: include CLIENT.SSL flag and wrap the socket. + _do_ssl = True + client_flags |= CLIENT.SSL + elif self._ssl_required: + raise err.OperationalError( + CR.CR_SSL_CONNECTION_ERROR, + "SSL is required but the server doesn't support it", + ) + else: + # PREFERRED mode: server doesn't support SSL, fall back to non-SSL. + _do_ssl = False + else: + _do_ssl = False + data_init = struct.pack( - "<iIB23s", self.client_flag, MAX_PACKET_LEN, charset_id, b"" + "<iIB23s", client_flags, MAX_PACKET_LEN, charset_id, b"" ) - if self.ssl and self.server_capabilities & CLIENT.SSL: + if _do_ssl: self.write_packet(data_init) - self._sock = self.ctx.wrap_socket(self._sock, server_hostname=self.host) self._rfile = self._sock.makefile("rb") self._secure = True @@ -923,7 +965,7 @@ print("caching_sha2: empty password") elif self._auth_plugin_name == "sha256_password": plugin_name = b"sha256_password" - if self.ssl and self.server_capabilities & CLIENT.SSL: + if _do_ssl: authresp = self.password + b"\0" elif self.password: authresp = b"\1" # request public key diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyMySQL-1.1.2/pymysql/converters.py new/PyMySQL-1.2.0/pymysql/converters.py --- old/PyMySQL-1.1.2/pymysql/converters.py 2025-08-24 14:53:42.000000000 +0200 +++ new/PyMySQL-1.2.0/pymysql/converters.py 2026-05-19 09:48:58.000000000 +0200 @@ -135,6 +135,8 @@ def Decimal2Literal(o, d): + if not o.is_finite(): + raise ProgrammingError("%s can not be used with MySQL" % str(o).lower()) return format(o, "f") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyMySQL-1.1.2/pymysql/cursors.py new/PyMySQL-1.2.0/pymysql/cursors.py --- old/PyMySQL-1.1.2/pymysql/cursors.py 2025-08-24 14:53:42.000000000 +0200 +++ new/PyMySQL-1.2.0/pymysql/cursors.py 2026-05-19 09:48:58.000000000 +0200 @@ -1,5 +1,4 @@ import re -import warnings from . import err @@ -8,12 +7,19 @@ #: You can use it to load large dataset. RE_INSERT_VALUES = re.compile( r"\s*((?:INSERT|REPLACE)\b.+\bVALUES?\s*)" - + r"(\(\s*(?:%s|%\(.+\)s)\s*(?:,\s*(?:%s|%\(.+\)s)\s*)*\))" - + r"(\s*(?:ON DUPLICATE.*)?);?\s*\Z", + + r"(\(\s*(?:%s|%\([^)]+\)s)\s*(?:,\s*(?:%s|%\([^)]+\)s)\s*)*\))" + + r'(\s*(?:AS\s+(?:`[^`]+`|"[^"]+"|[0-9A-Za-z_$]+)\s*' + + r'(?:\(\s*(?:`[^`]+`|"[^"]+"|[0-9A-Za-z_$]+)\s*' + + r'(?:,\s*(?:`[^`]+`|"[^"]+"|[0-9A-Za-z_$]+)\s*)*\))?\s*)?' + + r"(?:ON DUPLICATE.*)?);?\s*\Z", re.IGNORECASE | re.DOTALL, ) +def _backquote_escape(s): + return s.replace("`", "``") + + class Cursor: """ This is the object used to interact with the database. @@ -251,9 +257,11 @@ to advance through all result sets; otherwise you may get disconnected. """ + procname_escaped = _backquote_escape(procname) conn = self._get_db() + if args: - fmt = f"@_{procname}_%d=%s" + fmt = f"@`_{procname_escaped}_%d`=%s" self._query( "SET %s" % ",".join( @@ -262,9 +270,9 @@ ) self.nextset() - q = "CALL {}({})".format( - procname, - ",".join(["@_%s_%d" % (procname, i) for i in range(len(args))]), + q = "CALL `{}`({})".format( + procname_escaped, + ",".join([f"@`_{procname_escaped}_{i}`" for i in range(len(args))]), ) self._query(q) self._executed = q @@ -353,30 +361,6 @@ raise StopIteration return row - 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: # You can override this to use OrderedDict or other dict-like types. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyMySQL-1.1.2/pymysql/err.py new/PyMySQL-1.2.0/pymysql/err.py --- old/PyMySQL-1.1.2/pymysql/err.py 2025-08-24 14:53:42.000000000 +0200 +++ new/PyMySQL-1.2.0/pymysql/err.py 2026-05-19 09:48:58.000000000 +0200 @@ -16,6 +16,10 @@ """Exception that is the base class of all other error exceptions (not Warning).""" + def __init__(self, *args, sqlstate=None): + super().__init__(*args) + self.sqlstate = sqlstate + class InterfaceError(Error): """Exception raised for errors that are related to the database @@ -138,13 +142,13 @@ errno = struct.unpack("<h", data[1:3])[0] # https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_err_packet.html # Error packet has optional sqlstate that is 5 bytes and starts with '#'. + sqlstate = None if data[3] == 0x23: # '#' - # sqlstate = data[4:9].decode() - # TODO: Append (sqlstate) in the error message. This will be come in next minor release. + sqlstate = data[4:9].decode() errval = data[9:].decode("utf-8", "replace") else: errval = data[3:].decode("utf-8", "replace") errorclass = error_map.get(errno) if errorclass is None: errorclass = InternalError if errno < 1000 else OperationalError - raise errorclass(errno, errval) + raise errorclass(errno, errval, sqlstate=sqlstate) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyMySQL-1.1.2/pymysql/tests/test_connection.py new/PyMySQL-1.2.0/pymysql/tests/test_connection.py --- old/PyMySQL-1.1.2/pymysql/tests/test_connection.py 2025-08-24 14:53:42.000000000 +0200 +++ new/PyMySQL-1.2.0/pymysql/tests/test_connection.py 2026-05-19 09:48:58.000000000 +0200 @@ -810,6 +810,87 @@ ) assert not create_default_context.called + # PREFERRED mode: no SSL options specified → attempt SSL but don't require it + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) + with mock.patch( + "pymysql.connections.ssl.create_default_context", + new=mock.Mock(return_value=dummy_ssl_context), + ) as create_default_context: + conn = pymysql.connect(defer_connect=True) + assert create_default_context.called + assert conn.ssl is True + assert conn._ssl_required is False + assert not dummy_ssl_context.check_hostname + assert dummy_ssl_context.verify_mode == ssl.CERT_NONE + + def test_ssl_required_error(self): + """REQUIRED mode raises OperationalError when server doesn't support SSL.""" + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) + mock_create_ctx = mock.Mock(return_value=dummy_ssl_context) + with mock.patch( + "pymysql.connections.ssl.create_default_context", + new=mock_create_ctx, + ): + conn = pymysql.connect(ssl_ca="ca", defer_connect=True) + # Verify the context was created with the CA certificate + mock_create_ctx.assert_called_once_with(cafile="ca", capath=None) + + assert conn.ssl is True + assert conn._ssl_required is True + + # Simulate a server that doesn't advertise SSL support + conn.server_version = "8.0.0" + conn.server_capabilities = 0 # no CLIENT.SSL bit + conn.client_flag = 0 + conn.charset = "utf8mb4" + conn.user = "root" + conn.encoding = "utf8" + + with pytest.raises( + pymysql.err.OperationalError, + match="SSL is required but the server doesn't support it", + ): + conn._request_authentication() + + def test_ssl_preferred_no_server_ssl(self): + """PREFERRED mode falls back silently when server doesn't support SSL.""" + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) + with mock.patch( + "pymysql.connections.ssl.create_default_context", + new=mock.Mock(return_value=dummy_ssl_context), + ): + conn = pymysql.connect(defer_connect=True) + + assert conn.ssl is True + assert conn._ssl_required is False + + # Simulate a server that doesn't advertise SSL support + conn.server_version = "8.0.0" + conn.server_capabilities = 0 # no CLIENT.SSL bit + conn.client_flag = 0 + conn.charset = "utf8mb4" + conn.user = "root" + conn.encoding = "utf8" + conn.salt = b"12345678901234567890" + conn._auth_plugin_name = "" + conn._next_seq_id = 0 + conn.db = None + + # Mock the socket write/read so we don't need a real server + with ( + mock.patch.object(conn, "_write_bytes"), + mock.patch.object(conn, "_read_packet") as mock_read, + ): + mock_pkt = mock.Mock() + mock_pkt.is_auth_switch_request.return_value = False + mock_pkt.is_extra_auth_data.return_value = False + mock_read.return_value = mock_pkt + # Should NOT raise OperationalError for SSL + conn._request_authentication() + + # Connection is not secure (no SSL upgrade happened) + assert not conn._secure + # A custom type and function to escape it class Foo: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyMySQL-1.1.2/pymysql/tests/test_converters.py new/PyMySQL-1.2.0/pymysql/tests/test_converters.py --- old/PyMySQL-1.1.2/pymysql/tests/test_converters.py 2025-08-24 14:53:42.000000000 +0200 +++ new/PyMySQL-1.2.0/pymysql/tests/test_converters.py 2026-05-19 09:48:58.000000000 +0200 @@ -1,6 +1,8 @@ import datetime +from decimal import Decimal from unittest import TestCase from pymysql import converters +from pymysql.err import ProgrammingError __all__ = ["TestConverter"] @@ -52,3 +54,16 @@ expected = datetime.time(23, 6, 20, 511581) time_obj = converters.convert_time("23:06:20.511581") self.assertEqual(time_obj, expected) + + def test_decimal_special_values(self): + values = ( + Decimal("NaN"), + Decimal("sNaN"), + Decimal("Infinity"), + Decimal("-Infinity"), + ) + for value in values: + with self.assertRaisesRegex( + ProgrammingError, f"{str(value).lower()} can not be used with MySQL" + ): + converters.Decimal2Literal(value, None) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyMySQL-1.1.2/pymysql/tests/test_cursor.py new/PyMySQL-1.2.0/pymysql/tests/test_cursor.py --- old/PyMySQL-1.1.2/pymysql/tests/test_cursor.py 2025-08-24 14:53:42.000000000 +0200 +++ new/PyMySQL-1.2.0/pymysql/tests/test_cursor.py 2026-05-19 09:48:58.000000000 +0200 @@ -5,6 +5,31 @@ import pytest +def test_re_insert_values_with_on_duplicate_key_alias(): + m = pymysql.cursors.RE_INSERT_VALUES.match( + "INSERT INTO t1 (a,b,c) VALUES (%s,%s,%s) AS new " + "ON DUPLICATE KEY UPDATE c = new.a + new.b" + ) + assert m is not None + assert m.group(1) == "INSERT INTO t1 (a,b,c) VALUES " + assert m.group(2) == "(%s,%s,%s)" + assert m.group(3) == " AS new ON DUPLICATE KEY UPDATE c = new.a + new.b" + + m = pymysql.cursors.RE_INSERT_VALUES.match( + "INSERT INTO t1 (a,b,c) VALUES (%s,%s,%s) AS new(n1,n2,n3) " + "ON DUPLICATE KEY UPDATE c = n1 + n2" + ) + assert m is not None + assert m.group(3) == " AS new(n1,n2,n3) ON DUPLICATE KEY UPDATE c = n1 + n2" + + m = pymysql.cursors.RE_INSERT_VALUES.match( + "INSERT INTO t1 (a,b,c) VALUES (%s,%s,%s) " + "ON DUPLICATE KEY UPDATE c=VALUES(a)+VALUES(b)" + ) + assert m is not None + assert m.group(3) == " ON DUPLICATE KEY UPDATE c=VALUES(a)+VALUES(b)" + + class CursorTest(base.PyMySQLTestCase): def setUp(self): super().setUp() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyMySQL-1.1.2/pymysql/tests/test_err.py new/PyMySQL-1.2.0/pymysql/tests/test_err.py --- old/PyMySQL-1.1.2/pymysql/tests/test_err.py 2025-08-24 14:53:42.000000000 +0200 +++ new/PyMySQL-1.2.0/pymysql/tests/test_err.py 2026-05-19 09:48:58.000000000 +0200 @@ -1,5 +1,18 @@ import pytest +from unittest import mock + from pymysql import err +from pymysql.connections import Connection + + +def test_error_init_sqlstate(): + error = err.Error(1234, "boom", sqlstate="42000") + assert error.args == (1234, "boom") + assert error.sqlstate == "42000" + + error = err.Error(1234, "boom") + assert error.args == (1234, "boom") + assert error.sqlstate is None def test_raise_mysql_exception(): @@ -8,9 +21,21 @@ err.raise_mysql_exception(data) assert cm.type == err.OperationalError assert cm.value.args == (1045, "Access denied") + assert cm.value.sqlstate == "28000" data = b"\xff\x10\x04Too many connections" with pytest.raises(err.OperationalError) as cm: err.raise_mysql_exception(data) assert cm.type == err.OperationalError assert cm.value.args == (1040, "Too many connections") + assert cm.value.sqlstate is None + + +def test_set_charset_deprecated(): + con = mock.Mock(spec=Connection) + with pytest.warns( + DeprecationWarning, + match="'set_charset' is deprecated, use 'set_character_set' instead", + ): + Connection.set_charset(con, "utf8mb4") + con.set_character_set.assert_called_once_with("utf8mb4") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyMySQL-1.1.2/pymysql/tests/test_issues.py new/PyMySQL-1.2.0/pymysql/tests/test_issues.py --- old/PyMySQL-1.1.2/pymysql/tests/test_issues.py 2025-08-24 14:53:42.000000000 +0200 +++ new/PyMySQL-1.2.0/pymysql/tests/test_issues.py 2026-05-19 09:48:58.000000000 +0200 @@ -1,6 +1,7 @@ import datetime import time import warnings +from textwrap import dedent import pytest @@ -357,7 +358,9 @@ c.execute("""select @@autocommit;""") self.assertFalse(c.fetchone()[0]) conn.close() - conn.ping() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + conn.ping(reconnect=True) c.execute("""select @@autocommit;""") self.assertFalse(c.fetchone()[0]) conn.close() @@ -368,7 +371,9 @@ c.execute("""select @@autocommit;""") self.assertFalse(c.fetchone()[0]) conn.close() - conn.ping() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + conn.ping(reconnect=True) conn.autocommit(True) c.execute("""select @@autocommit;""") self.assertTrue(c.fetchone()[0]) @@ -496,3 +501,23 @@ # don't assert the exact internal binary value, as it could # vary across implementations self.assertTrue(isinstance(row[0], bytes)) + + def test_issue_1206(self): + conn = pymysql.connect(charset="utf8", **self.databases[0]) + + cur = conn.cursor() + cur.execute("DROP PROCEDURE IF EXISTS `foo.bar`") + try: + cur.execute( + dedent("""\ + create procedure `foo.bar` (arg1 int) + begin + select arg1*2; + end + """) + ) + + cur.callproc("foo.bar", args=(123,)) + self.assertEqual(cur.fetchone()[0], 246) + finally: + cur.execute("DROP PROCEDURE IF EXISTS `foo.bar`") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyMySQL-1.1.2/pyproject.toml new/PyMySQL-1.2.0/pyproject.toml --- old/PyMySQL-1.1.2/pyproject.toml 2025-08-24 14:53:42.000000000 +0200 +++ new/PyMySQL-1.2.0/pyproject.toml 2026-05-19 09:48:58.000000000 +0200 @@ -7,7 +7,7 @@ ] dependencies = [] -requires-python = ">=3.8" +requires-python = ">=3.9" readme = "README.md" license = "MIT" keywords = ["MySQL"] @@ -22,10 +22,10 @@ [project.optional-dependencies] "rsa" = [ - "cryptography" + "cryptography>=46.0.7" ] "ed25519" = [ - "PyNaCl>=1.4.0" + "PyNaCl>=1.6.2" ] [project.urls]
