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]

Reply via email to