diff --git a/.coveragerc b/.coveragerc
index a9ec99425..efa9a2ff8 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/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
deleted file mode 100644
index a4c434c5e..000000000
--- 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@v3
-
- # Initializes the CodeQL tools for scanning.
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v2
- 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@v2
-
- # âšī¸ 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@v2
diff --git a/.github/workflows/django.yaml b/.github/workflows/django.yaml
index da664f856..5c4609543 100644
--- a/.github/workflows/django.yaml
+++ b/.github/workflows/django.yaml
@@ -36,10 +36,10 @@ 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
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index 77edb0c38..07ea66031 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -13,11 +13,12 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- - uses: psf/black@stable
- with:
- options: "--check --verbose"
- src: "."
+ - uses: astral-sh/ruff-action@v3
- - uses: chartboost/ruff-action@v1
+ - name: format
+ run: ruff format --diff
+
+ - name: lint
+ run: ruff check --diff
diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml
deleted file mode 100644
index 780dd92d7..000000000
--- 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@v4
-
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 6b1e0f321..b09b2269c 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
@@ -15,27 +18,37 @@ jobs:
fail-fast: false
matrix:
include:
- - db: "mariadb:10.4"
- py: "3.8"
+ - db: "mariadb:12" # rolling release
+ py: "3.13"
+ # pynacl 1.5 doesn't support Python 3.14 yet.
+ # https://github.com/pyca/pynacl/commit/d33028e43b814615a33e231925eaddb0f679fa2b
- - db: "mariadb:10.5"
- py: "3.7"
+ - db: "mariadb:11.8"
+ py: "3.12"
- - db: "mariadb:10.6"
+ - db: "mariadb:11.4"
py: "3.11"
- - db: "mariadb:lts"
- py: "3.9"
+ - db: "mariadb:10.11"
+ py: "3.10"
- - db: "mysql:5.7"
- py: "pypy-3.8"
+ - db: "mariadb:10.6"
+ py: "3.9"
- db: "mysql:8.0"
+ py: "pypy3.11"
+ mysql_auth: true
+
+ - db: "mysql:9"
+ py: "3.14"
+ mysql_auth: true
+
+ - db: "mysql:8.4"
py: "3.9"
mysql_auth: true
- - db: "mysql:8.0"
- py: "3.10"
+ - db: "mysql:5.7"
+ py: "3.9"
services:
mysql:
@@ -50,7 +63,7 @@ jobs:
- /run/mysqld:/run/mysqld
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v5
- name: Workaround MySQL container permissions
if: startsWith(matrix.db, 'mysql')
@@ -59,9 +72,10 @@ 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
cache: 'pip'
cache-dependency-path: 'requirements-dev.txt'
@@ -99,4 +113,4 @@ jobs:
- name: Upload coverage reports to Codecov
if: github.repository == 'PyMySQL/PyMySQL'
- uses: codecov/codecov-action@v3
+ uses: codecov/codecov-action@v5
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 000000000..59fdb65df
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,17 @@
+# .readthedocs.yaml
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+version: 2
+
+build:
+ os: ubuntu-22.04
+ tools:
+ python: "3.12"
+
+python:
+ install:
+ - requirements: docs/requirements.txt
+
+# Build documentation in the docs/ directory with Sphinx
+sphinx:
+ configuration: docs/source/conf.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ea1d732a9..d78d2a010 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,44 @@
# 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.
+* `Connection.ping(reconnect)` change the default to not reconnect.
+
+## v1.1.2
+
+Release date: 2025-08-24
+
+* Prevent UnboundLocalError in very rare situation. https://github.com/PyMySQL/PyMySQL/pull/1174
+* Close underlying `SocketIO` soon when Connection is closed for PyPy. https://github.com/PyMySQL/PyMySQL/issues/1183
+* Fix importing PyMySQL fail on CPython 3.13 when `getpass.getuser()` raises OSEError. https://github.com/PyMySQL/PyMySQL/pull/1190
+* Make charset name "utf8" case insensitive. https://github.com/PyMySQL/PyMySQL/pull/1195
+* `Connection.kill()` uses `KILL` query instead of `COM_KILL`command to support MySQL 8.4. https://github.com/PyMySQL/PyMySQL/pull/1197
+* Fix SSL error on CPython 3.13 caused by strict TLS default setting. https://github.com/PyMySQL/PyMySQL/pull/1198
+* Fix auth switch request handling. https://github.com/PyMySQL/PyMySQL/pull/1200
+
+
+## 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)
+* Added ssl_key_password param. #1145
+
## 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 +46,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 +60,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/README.md b/README.md
index 32f5df2f4..ba491e2df 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,20 @@
[](https://pymysql.readthedocs.io/)
[](https://codecov.io/gh/PyMySQL/PyMySQL)
+[](https://deepwiki.com/PyMySQL/PyMySQL)
# PyMySQL
-This package contains a pure-Python MySQL 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
- Python -- one of the following:
- - [CPython](https://www.python.org/) : 3.7 and newer
+ - [CPython](https://www.python.org/) : 3.9 and newer
- [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.4
+ - [MySQL](https://www.mysql.com/) LTS versions
+ - [MariaDB](https://mariadb.org/) LTS versions
## Installation
@@ -92,6 +93,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:
diff --git a/docs/Makefile b/docs/Makefile
index d37255520..c1240d2ba 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 dcd4287c6..000000000
--- 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/requirements.txt b/docs/requirements.txt
new file mode 100644
index 000000000..48319f033
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,2 @@
+sphinx~=8.0
+sphinx-rtd-theme~=3.0.0
diff --git a/docs/source/conf.py b/docs/source/conf.py
index a57a03c44..158d0d12f 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.
#
@@ -30,7 +28,6 @@
# ones.
extensions = [
"sphinx.ext.autodoc",
- "sphinx.ext.intersphinx",
]
# Add any paths that contain templates here, relative to this directory.
@@ -47,7 +44,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
@@ -101,7 +98,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
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 97633f1aa..e64b64238 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
diff --git a/docs/source/user/development.rst b/docs/source/user/development.rst
index 1f8a2637f..2d80a6248 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
diff --git a/pymysql/__init__.py b/pymysql/__init__.py
index 68d7043b6..0ec7ae6eb 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
@@ -48,13 +49,13 @@
# PyMySQL version.
# Used by setuptools and connection_attrs
-VERSION = (1, 1, 0, "rc", 2)
-VERSION_STRING = "1.1.0rc2"
+VERSION = (1, 1, 2, "final")
+VERSION_STRING = "1.1.2"
### 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
diff --git a/pymysql/_auth.py b/pymysql/_auth.py
index 99987b770..4790449b8 100644
--- a/pymysql/_auth.py
+++ b/pymysql/_auth.py
@@ -1,6 +1,7 @@
"""
Implements auth methods
"""
+
from .err import OperationalError
@@ -165,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:
@@ -214,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/charset.py b/pymysql/charset.py
index b1c1ca8b8..ec8e14e21 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/connections.py b/pymysql/connections.py
index 843bea5e0..99fcfcd0b 100644
--- a/pymysql/connections.py
+++ b/pymysql/connections.py
@@ -40,11 +40,14 @@
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
+_DEFAULT_AUTH_PLUGIN = None # if this is not None, use it instead of server's default.
TEXT_TYPES = {
FIELD_TYPE.BIT,
@@ -84,8 +87,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
diff --git a/pymysql/tests/test_SSCursor.py b/pymysql/tests/test_SSCursor.py
index 9cb5bafed..d5e6e2bce 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 e77605fdf..0fe13b59d 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):
@@ -364,7 +365,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 +415,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_charset.py b/pymysql/tests/test_charset.py
index 94e6e1559..1dbe6fffa 100644
--- a/pymysql/tests/test_charset.py
+++ b/pymysql/tests/test_charset.py
@@ -21,5 +21,24 @@ 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
diff --git a/pymysql/tests/test_connection.py b/pymysql/tests/test_connection.py
index 0803efc92..1a16c982a 100644
--- a/pymysql/tests/test_connection.py
+++ b/pymysql/tests/test_connection.py
@@ -558,8 +558,8 @@ def test_defer_connect(self):
sock.close()
def test_ssl_connect(self):
- dummy_ssl_context = mock.Mock(options=0)
- with mock.patch("pymysql.connections.Connection.connect"), mock.patch(
+ 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:
@@ -570,15 +570,20 @@ def test_ssl_connect(self):
"key": "key",
"cipher": "cipher",
},
+ defer_connect=True,
)
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)
- with mock.patch("pymysql.connections.Connection.connect"), mock.patch(
+ 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:
@@ -588,20 +593,50 @@ def test_ssl_connect(self):
"cert": "cert",
"key": "key",
},
+ defer_connect=True,
)
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(
+ 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:
+ pymysql.connect(
+ ssl={
+ "ca": "ca",
+ "cert": "cert",
+ "key": "key",
+ "password": "password",
+ },
+ defer_connect=True,
+ )
+ 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, verify_flags=0)
+ 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
@@ -609,8 +644,8 @@ 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)
- with mock.patch("pymysql.connections.Connection.connect"), mock.patch(
+ 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:
@@ -618,16 +653,21 @@ 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
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"):
- dummy_ssl_context = mock.Mock(options=0)
- with mock.patch("pymysql.connections.Connection.connect"), mock.patch(
+ 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:
@@ -635,18 +675,21 @@ 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
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
for ssl_verify_cert in (None, False, "0", "no", "false"):
- dummy_ssl_context = mock.Mock(options=0)
- with mock.patch("pymysql.connections.Connection.connect"), mock.patch(
+ 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:
@@ -654,19 +697,22 @@ 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
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
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"), mock.patch(
+ 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:
@@ -675,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
@@ -682,12 +729,14 @@ 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
- dummy_ssl_context = mock.Mock(options=0)
- with mock.patch("pymysql.connections.Connection.connect"), mock.patch(
+ 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:
@@ -696,15 +745,43 @@ 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
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, verify_flags=0)
+ 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",
+ ssl_cert="cert",
+ 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
+ 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)
- with mock.patch("pymysql.connections.Connection.connect"), mock.patch(
+ 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:
@@ -715,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)
- with mock.patch("pymysql.connections.Connection.connect"), mock.patch(
+ 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:
@@ -728,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
@@ -781,12 +860,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()
@@ -813,3 +895,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
diff --git a/pymysql/tests/test_cursor.py b/pymysql/tests/test_cursor.py
index b292c2068..2e267fb6a 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_err.py b/pymysql/tests/test_err.py
index 6b54c6d04..6eb0f987d 100644
--- a/pymysql/tests/test_err.py
+++ b/pymysql/tests/test_err.py
@@ -1,14 +1,16 @@
-import unittest
-
+import pytest
from pymysql import err
-__all__ = ["TestRaiseException"]
-
+def test_raise_mysql_exception():
+ data = b"\xff\x15\x04#28000Access denied"
+ with pytest.raises(err.OperationalError) as cm:
+ err.raise_mysql_exception(data)
+ assert cm.type == err.OperationalError
+ assert cm.value.args == (1045, "Access denied")
-class TestRaiseException(unittest.TestCase):
- def test_raise_mysql_exception(self):
- data = b"\xff\x15\x04#28000Access denied"
- with self.assertRaises(err.OperationalError) as cm:
- err.raise_mysql_exception(data)
- self.assertEqual(cm.exception.args, (1045, "Access denied"))
+ 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")
diff --git a/pymysql/tests/test_issues.py b/pymysql/tests/test_issues.py
index 3564d3a6e..f1fe8dd48 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 4b6b2a779..a10f8d5b7 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 838512955..fff14b86f 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 c68289fe8..5c34d40d1 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 18714779d..d5057982f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,22 +7,15 @@ authors = [
]
dependencies = []
-requires-python = ">=3.7"
+requires-python = ">=3.8"
readme = "README.md"
-license = {text = "MIT License"}
+license = "MIT"
keywords = ["MySQL"]
classifiers = [
"Development Status :: 5 - Production/Stable",
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.7",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Intended Audience :: Developers",
- "License :: OSI Approved :: MIT License",
"Topic :: Database",
]
dynamic = ["version"]
@@ -38,9 +31,10 @@ dynamic = ["version"]
[project.urls]
"Project" = "https://github.com/PyMySQL/PyMySQL"
"Documentation" = "https://pymysql.readthedocs.io/"
+"Changelog" = "https://github.com/PyMySQL/PyMySQL/blob/main/CHANGELOG.md"
[build-system]
-requires = ["setuptools>=61"]
+requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
@@ -49,14 +43,16 @@ include = ["pymysql*"]
exclude = ["tests*", "pymysql.tests*"]
[tool.setuptools.dynamic]
-version = {attr = "pymysql.VERSION"}
+version = {attr = "pymysql.VERSION_STRING"}
[tool.ruff]
-line-length = 99
exclude = [
"pymysql/tests/thirdparty",
]
+[tool.ruff.lint]
+ignore = ["E721"]
+
[tool.pdm.dev-dependencies]
dev = [
"pytest-cov>=4.0.0",
diff --git a/renovate.json b/renovate.json
index 39a2b6e9a..09e16da6b 100644
--- a/renovate.json
+++ b/renovate.json
@@ -2,5 +2,6 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
- ]
+ ],
+ "dependencyDashboard": false
}
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 13d7f7fb4..140d37067 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -2,4 +2,3 @@ cryptography
PyNaCl>=1.4.0
pytest
pytest-cov
-coveralls
diff --git a/tests/test_auth.py b/tests/test_auth.py
index e5e2a64e5..d7a0e82fc 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