Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/actions.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ workflows:
'.github/workflows/windows.yaml':
- 'actions/cache@v6.1.0'
- 'actions/checkout@v7.0.1'
- 'actions/download-artifact@v8.0.2'
- 'actions/setup-python@v7.0.0'
- 'actions/upload-artifact@v7.0.2'
- 'pypa/gh-action-pypi-publish@v1.14.3'
dependencies:
'actions/cache@v6.1.0':
ref: 'v6.1.0'
Expand All @@ -25,11 +27,21 @@ dependencies:
commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1'
owner_id: 44036562
repo_id: 197814629
'actions/download-artifact@v8.0.2':
ref: 'v8.0.2'
commit: 'sha1-9000827ccba6bdab643e8b6fd33ac0654aef8333'
owner_id: 44036562
repo_id: 192626254
'actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd':
ref: 'v8.0.0'
commit: 'sha1-ed597411d8f924073f98dfc5c65a23a2325f34cd'
owner_id: 44036562
repo_id: 205262760
'actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405':
ref: 'v6.2.0'
commit: 'sha1-a309ff8b426b58ec0e2a45f0f869d46889d02405'
owner_id: 44036562
repo_id: 192625525
'actions/setup-python@v7.0.0':
ref: 'v7.0.0'
commit: 'sha1-5fda3b95a4ea91299a34e894583c3862153e4b97'
Expand All @@ -47,3 +59,10 @@ dependencies:
repo_id: 200299178
uses:
- 'actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd'
'pypa/gh-action-pypi-publish@v1.14.3':
ref: 'v1.14.3'
commit: 'sha1-7245906fb61fa33a13554f9d5fb856fe61758086'
owner_id: 647025
repo_id: 178055147
uses:
- 'actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405'
27 changes: 27 additions & 0 deletions .github/workflows/windows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ name: Build windows wheels
on:
push:
branches: ["main", "ci"]
tags: ["v*"]
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
build:
strategy:
Expand Down Expand Up @@ -107,3 +111,26 @@ jobs:
with:
name: win-wheels-${{ matrix.cibw_arch }}
path: mysqlclient/dist/*.*
if-no-files-found: error

publish:
name: Publish to PyPI
needs: build
if: >-
github.event_name == 'push' && github.event.created &&
startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- name: Download distributions
uses: actions/download-artifact@v8.0.2
with:
pattern: win-wheels-*
path: dist
merge-multiple: true

- name: Publish distributions to PyPI
uses: pypa/gh-action-pypi-publish@v1.14.3
with:
attestations: true
21 changes: 21 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
======================
What's new in 2.3.1
======================

Release: 2026-11-10

* **Security fix:** Escape Python ``set`` query parameters using the connection's
character set and SQL mode. This fixes SQL injection when
``NO_BACKSLASH_ESCAPES`` is enabled and an attacker controls a set member.
``frozenset`` parameters now use the same escaping.
See `GHSA-m9c3-99x3-77g6 <https://github.com/PyMySQL/mysqlclient/security/advisories/GHSA-m9c3-99x3-77g6>`_.

* Deprecate ``MySQLdb.converters.Set2Str``.

* ``Connection.literal()`` now applies built-in handling only to exact built-in
types. Subclasses use the connection's converter mapping.

* Fix crashes when looking up connection attributes with a non-string name or
a string containing lone surrogates. (#810)


======================
What's new in 2.3.0
======================
Expand Down
105 changes: 105 additions & 0 deletions notes/release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Release procedure

The examples below release **2.3.1** after **2.3.0**. Replace the version
numbers and release date as needed. Run commands from the repository root.

Ensure the PyPI Trusted Publisher for `mysqlclient` is configured with
repository `PyMySQL/mysqlclient`, workflow `windows.yaml`, and no required
environment name.

## 1. Update the release notes

Review the changes since the previous release:

```sh
git log v2.3.0..HEAD --oneline
git diff v2.3.0..HEAD --stat
```

Add a `What's new in 2.3.1` section at the top of
[HISTORY.rst](../HISTORY.rst), with `Release: YYYY-MM-DD` set to the release
date. Document changes, fixes, and deprecations that affect users.

## 2. Update the version and create a release PR

Update both version fields in
[src/MySQLdb/release.py](../src/MySQLdb/release.py):

```python
__version__ = "2.3.1"
version_info = (2, 3, 1, "final", 0)
```

Format the source and run the checks:

```sh
ruff format src/
ruff check src/
git diff --check
```

Review the diff, commit it as `release v2.3.1`, and open a PR targeting
`main` in `PyMySQL/mysqlclient`.

## 3. Verify CI for the release commit

Once the PR checks pass, merge it into `main`. Verify that all of the
following workflows pass for the merged commit:

- `Lint`
- `Test`, including MySQL, MariaDB, and Django tests
- `Build windows wheels`

To run the tests locally, install the package and the dependencies in
`requirements.txt`, prepare the database, and run the command used by the
[test workflow](../.github/workflows/tests.yaml). Adjust `TESTDB` to use
the configuration for your database:

```sh
TESTDB=actions.cnf pytest --cov=MySQLdb tests
```

Verify that the [Windows workflow](../.github/workflows/windows.yaml)
produces both `win-wheels-AMD64` and `win-wheels-ARM64` artifacts. The AMD64
artifact also contains the source distribution (sdist).

Record the release commit SHA.

## 4. Create and push the release tag

Replace `RELEASE_SHA` with the commit SHA verified in step 3. Fetch the
commit, create a lightweight tag, and push it:

```sh
git fetch upstream main --tags
git tag v2.3.1 RELEASE_SHA
git push upstream refs/tags/v2.3.1
```

Pushing the new `v*` tag starts a build of the tagged commit. Once both
Windows builds succeed, the `Publish to PyPI` job combines their artifacts
and publishes the wheels and sdist using Trusted Publishing.
Publishing is restricted to push events that create a new `v*` tag.

## 5. Verify publication and create the GitHub Release

Check the tag-triggered `Build windows wheels` workflow and confirm that
both builds and the `Publish to PyPI` job succeed. Then create the GitHub
Release:

```sh
gh release create v2.3.1 -R PyMySQL/mysqlclient \
--verify-tag --title v2.3.1 \
--generate-notes --notes-start-tag v2.3.0
```

## 6. Verify the published release

Check that PyPI lists the sdist and wheels for both architectures. Install
`mysqlclient==2.3.1` in a fresh environment and verify the import and version:

```sh
python -c 'import MySQLdb; print(MySQLdb.version_info)'
```

The expected output is `(2, 3, 1, 'final', 0)`.
19 changes: 13 additions & 6 deletions src/MySQLdb/connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,7 @@ class object, used to create cursors (keyword only)
"executemany_fallback", self.executemany_fallback
)
if executemany_fallback not in ("loop", "multi"):
raise ValueError(
"executemany_fallback must be either 'loop' or 'multi'"
)
raise ValueError("executemany_fallback must be either 'loop' or 'multi'")

client_flag = kwargs.get("client_flag", 0)
client_flag |= CLIENT.MULTI_RESULTS
Expand Down Expand Up @@ -311,6 +309,12 @@ def _bytes_literal(self, bs):
def _tuple_literal(self, t):
return b"(%s)" % (b",".join(map(self.literal, t)))

def _set_literal(self, s):
for c in s:
if not isinstance(c, str):
raise TypeError("set elements must be strings")
return self.string_literal(",".join(s))

def literal(self, o):
"""If o is a single object, returns an SQL literal as a string.
If o is a non-string sequence, the items of the sequence are
Expand All @@ -319,12 +323,15 @@ def literal(self, o):
Non-standard. For internal use; do not use this in your
applications.
"""
if isinstance(o, str):
t = type(o)
if t is str:
s = self.string_literal(o.encode(self.encoding))
elif isinstance(o, (bytes, bytearray)):
elif t in (bytes, bytearray):
s = self._bytes_literal(o)
elif isinstance(o, (tuple, list)):
elif t in (tuple, list):
s = self._tuple_literal(o)
elif t in (set, frozenset):
s = self._set_literal(o)
else:
s = self.escape(o, self.encoders)
if isinstance(s, str):
Expand Down
4 changes: 3 additions & 1 deletion src/MySQLdb/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ def Bool2Str(s, d):

def Set2Str(s, d):
# Only support ascii string. Not tested.
import warnings

warnings.warn("Set2Str is deprecated", DeprecationWarning, stacklevel=2)
return string_literal(",".join(s))


Expand Down Expand Up @@ -112,7 +115,6 @@ def array2Str(o, d):
Date: Thing2Literal,
DateTimeType: DateTime2literal,
DateTimeDeltaType: DateTimeDelta2literal,
set: Set2Str,
Decimal: Decimal2Literal,
FIELD_TYPE.TINY: int,
FIELD_TYPE.SHORT: int,
Expand Down
4 changes: 2 additions & 2 deletions src/MySQLdb/release.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__author__ = "Inada Naoki <songofacandy@gmail.com>"
__version__ = "2.3.0"
version_info = (2, 3, 0, "final", 0)
__version__ = "2.3.1"
version_info = (2, 3, 1, "final", 0)
Loading