From bc70b532317cbb6a9ad1b30d02c1c874ce50ca07 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 1 Sep 2026 11:10:35 +0000
Subject: [PATCH 1/6] Bump pydantic from 2.13.4 to 2.13.5 (#13615)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.13.4 to
2.13.5.
Release notes
Sourced from pydantic's
releases.
v2.13.5 (2026-08-28)
What's Changed
Fixes
- Allow reuse of validators when plugins are set by
@Viicos in #13535
- Fix missing GC traversal on some
pydantic-core struct
fields by @Viicos
in #13624
- Fix missing GC traversal in
pydantic-core for
GeneralFieldsSerializer by @Viicos in #13629
- Count validated model fields once in smart unions by
@tamird in #13731
Changelog
Sourced from pydantic's
changelog.
v2.13.5 (2026-08-28)
GitHub
release
What's Changed
Fixes
- Allow reuse of validators when plugins are set by
@Viicos in #13535
- Fix missing GC traversal on some
pydantic-core struct
fields by @Viicos
in #13624
- Fix missing GC traversal in
pydantic-core for
GeneralFieldsSerializer by @Viicos in #13629
- Count validated model fields once in smart unions by
@tamird in #13731
Commits
001dea0
Bump pypa/gh-action-pypi-publish action to v1.14.2
558379f
Bump twine to v7.0.0
2cfd5d3
Do not check for docs build
a735bee
Fix more Clippy lints
7eed4a1
Fix Clippy 0.1.95 warnings
b353bbb
Prepare release v2.13.5
63d2ccc
Count validated model fields once in smart unions
a53ec2e
Speed up PyPy CI tests
d65e0f9
Workaround circular import error in Mypy
47a6dbf
Fix missing GC traversal in pydantic-core for
GeneralFieldsSerializer
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 4 ++--
requirements/dev.txt | 4 ++--
requirements/lint.txt | 4 ++--
requirements/test-common.txt | 4 ++--
requirements/test-ft.txt | 4 ++--
requirements/test.txt | 4 ++--
6 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index a8d17be70ab..95235d555e0 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -187,9 +187,9 @@ pycares==5.0.1
# via aiodns
pycparser==3.0
# via cffi
-pydantic==2.13.4
+pydantic==2.13.5
# via python-on-whales
-pydantic-core==2.46.4
+pydantic-core==2.46.5
# via pydantic
pyenchant==3.3.0
# via sphinxcontrib-spelling
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 7b16b93d2bc..9f1893210ab 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -184,9 +184,9 @@ pycares==5.0.1
# via aiodns
pycparser==3.0
# via cffi
-pydantic==2.13.4
+pydantic==2.13.5
# via python-on-whales
-pydantic-core==2.46.4
+pydantic-core==2.46.5
# via pydantic
pygments==2.21.0
# via
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 041f3c5094c..5a41f837895 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -100,9 +100,9 @@ pycares==5.0.1
# via aiodns
pycparser==3.0
# via cffi
-pydantic==2.13.4
+pydantic==2.13.5
# via python-on-whales
-pydantic-core==2.46.4
+pydantic-core==2.46.5
# via pydantic
pygments==2.21.0
# via
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 4070c75cc96..3d9dc19c53d 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -80,9 +80,9 @@ proxy-py==2.4.10
# via -r requirements/test-common-base.in
pycparser==3.0
# via cffi
-pydantic==2.13.4
+pydantic==2.13.5
# via python-on-whales
-pydantic-core==2.46.4
+pydantic-core==2.46.5
# via pydantic
pygments==2.21.0
# via
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 304735580de..784dc9536b1 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -105,9 +105,9 @@ pycares==5.0.1
# via aiodns
pycparser==3.0
# via cffi
-pydantic==2.13.4
+pydantic==2.13.5
# via python-on-whales
-pydantic-core==2.46.4
+pydantic-core==2.46.5
# via pydantic
pygments==2.21.0
# via
diff --git a/requirements/test.txt b/requirements/test.txt
index 934575f3dd6..0143d3fd1dc 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -105,9 +105,9 @@ pycares==5.0.1
# via aiodns
pycparser==3.0
# via cffi
-pydantic==2.13.4
+pydantic==2.13.5
# via python-on-whales
-pydantic-core==2.46.4
+pydantic-core==2.46.5
# via pydantic
pygments==2.21.0
# via
From 986ee87a748603efde342e461d9062a049a10845 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 1 Sep 2026 11:35:00 +0000
Subject: [PATCH 2/6] Bump coverage from 7.15.4 to 7.16.0 (#13618)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.15.4
to 7.16.0.
Release notes
Sourced from coverage's
releases.
7.16.0
Version 7.16.0 — 2026-08-28
- When combining files, now path separator slashes will automatically
be converted to the local file system style. This makes it less
necessary to define
[paths] configuration to combine data
across operating systems. Fixes issue
2266.
- The Coverage.switch_context()
method now returns the previous context.
- Fix: previously, a
[paths] pattern would be replaced
everywhere in a file path when it was only meant to be replaced once, in
the leading portion of the path. This is now fixed, in pull
2268.
- Fixes to validation of options and configuration settings:
- Negative precision settings now always cause useful error messages
(pull
2261).
- An invalid regex in the
--contexts option (or the
[report] contexts setting) reported a confusing “Couldn’t
use data file …: user-defined function raised exception” error. Now it
raises a proper configuration error naming the bad regex, like other
regex settings do (pull
2262).
- Non-string values in TOML configuration settings now produce a
helpful error message instead of a traceback. This affects list settings
whose elements aren’t strings (like
omit,
exclude_lines, or a [paths] entry), file
settings like data_file, and any wrong-typed value in the
[paths] section (pull
2263).
coverage run refuses run-affecting command-line options
like --branch alongside
--concurrency=multiprocessing, since they can’t reach the
subprocesses. The check only recognized multiprocessing as
the entire option value, so
--concurrency=multiprocessing,thread slipped through and
failed later with “Can’t combine statement coverage data with branch
data”. Each named concurrency library is now properly considered (pull
2270).
- Fix:
coverage annotate -d DIR raised an
AssertionError if any measured file had an extension other
than .py, such as a .pyw file on Windows. The
original extension is now restored on the annotated copy (pull
2265).
:arrow_right: PyPI page: coverage 7.16.0.
:arrow_right: To install: python3 -m pip install
coverage==7.16.0
Changelog
Sourced from coverage's
changelog.
Version 7.16.0 — 2026-08-28
-
When combining files, now path separator slashes will automatically
be
converted to the local file system style. This makes it less necessary
to
define [paths] configuration to combine data across
operating systems.
Fixes issue 2266_.
-
The :meth:.Coverage.switch_context method now returns
the previous context.
-
Fix: previously, a [paths] pattern would be replaced
everywhere in a file
path when it was only meant to be replaced once, in the leading portion
of
the path. This is now fixed, in pull 2268_.
-
Fixes to validation of options and configuration settings:
-
Negative precision settings now always cause useful error messages
(pull 2261_).
-
An invalid regex in the --contexts option (or the
[report] contexts setting) reported a confusing
"Couldn't use data file ...:
user-defined function raised exception" error. Now it raises a
proper
configuration error naming the bad regex, like other regex settings do
(pull 2262_).
-
Non-string values in TOML configuration settings now produce a
helpful
error message instead of a traceback. This affects list settings whose
elements aren't strings (like omit,
exclude_lines, or a [paths]
entry), file settings like data_file, and any wrong-typed
value in the
[paths] section (pull 2263_).
-
coverage run refuses run-affecting command-line options
like
--branch alongside
--concurrency=multiprocessing, since they can't
reach the subprocesses. The check only recognized
multiprocessing as
the entire option value, so
--concurrency=multiprocessing,thread
slipped through and failed later with "Can't combine statement
coverage
data with branch data". Each named concurrency library is now
properly
considered (pull 2270_).
-
Fix: coverage annotate -d DIR raised an
AssertionError if any
measured file had an extension other than .py, such as a
.pyw file on
Windows. The original extension is now restored on the annotated copy
(pull 2265_).
.. _pull 2261: coveragepy/coveragepy#2261
.. _pull 2262: coveragepy/coveragepy#2262
.. _pull 2263: coveragepy/coveragepy#2263
.. _pull 2265: coveragepy/coveragepy#2265
.. _issue 2266: coveragepy/coveragepy#2266
.. _pull 2268: coveragepy/coveragepy#2268
... (truncated)
Commits
3e9fc16
docs: prep for 7.16.0
38be8d1
build: control check-manifest explicitly
8eb1266
docs(build): no longer commit sample_html
1a8b3fa
docs: remove sample_html
aeaa79b
docs: linklint is now sphinx-linklint
d5eaf3f
test: a branchless way to re-add extensions
57e52fd
docs: adjust CHANGES for #2270
b9d304d
fix: check for multiprocessing in a --concurrency list (#2270)
a6ef928
chore: make upgrade
070461f
chore: bump the action-dependencies group with 4 updates (#2271)
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/test-common-base.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test-mobile.txt | 2 +-
requirements/test.txt | 2 +-
7 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 95235d555e0..aa45dba00a6 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -65,7 +65,7 @@ click==8.5.0
# via
# pip-tools
# towncrier
-coverage==7.15.4
+coverage==7.16.0
# via
# -r requirements/test-common.in
# pytest-cov
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 9f1893210ab..4339b1ba6ae 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -65,7 +65,7 @@ click==8.5.0
# via
# pip-tools
# towncrier
-coverage==7.15.4
+coverage==7.16.0
# via
# -r requirements/test-common.in
# pytest-cov
diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt
index 709eaa1b642..2fc5b13681e 100644
--- a/requirements/test-common-base.txt
+++ b/requirements/test-common-base.txt
@@ -14,7 +14,7 @@ attrs==26.1.0
# via aiohttp
backports-asyncio-runner==1.2.0
# via pytest-asyncio
-coverage==7.15.4
+coverage==7.16.0
# via pytest-cov
exceptiongroup==1.3.1
# via pytest
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 3d9dc19c53d..7e828ff0243 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -22,7 +22,7 @@ blockbuster==1.5.27
# via -r requirements/test-common.in
cffi==2.1.1
# via cryptography
-coverage==7.15.4
+coverage==7.16.0
# via
# -r requirements/test-common.in
# pytest-cov
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 784dc9536b1..d42ce1a7c31 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -38,7 +38,7 @@ cffi==2.1.1
# via
# cryptography
# pycares
-coverage==7.15.4
+coverage==7.16.0
# via
# -r requirements/test-common.in
# pytest-cov
diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt
index 1cb597f14b4..3364b7fd15d 100644
--- a/requirements/test-mobile.txt
+++ b/requirements/test-mobile.txt
@@ -34,7 +34,7 @@ cffi==2.1.1 ; sys_platform != "android" and sys_platform != "ios"
# via
# -r requirements/test-mobile.in
# pycares
-coverage==7.15.4
+coverage==7.16.0
# via pytest-cov
exceptiongroup==1.3.1
# via
diff --git a/requirements/test.txt b/requirements/test.txt
index 0143d3fd1dc..87c7b97631a 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -38,7 +38,7 @@ cffi==2.1.1
# via
# cryptography
# pycares
-coverage==7.15.4
+coverage==7.16.0
# via
# -r requirements/test-common.in
# pytest-cov
From 6c7652e8b7af75ef966f174fa8d182981deaea94 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 1 Sep 2026 11:50:28 +0000
Subject: [PATCH 3/6] Bump python-discovery from 1.5.3 to 1.6.0 (#13617)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [python-discovery](https://github.com/tox-dev/python-discovery)
from 1.5.3 to 1.6.0.
Release notes
Sourced from python-discovery's
releases.
v1.6.0
What's Changed
Full Changelog: https://github.com/tox-dev/python-discovery/compare/1.5.3...1.6.0
Changelog
Sourced from python-discovery's
changelog.
Features - 1.6.0
- :attr:
~python_discovery.PythonInfo.system_exe returns
the executable of the system Python an interpreter is based on,
falling back to
:attr:~python_discovery.PythonInfo.executable the way
:meth:~python_discovery.PythonInfo.resolve_to_system does.
:attr:~python_discovery.PythonInfo.system_executable is
None until resolution runs, so every consumer had to narrow
a value discovery has already settled - by
:user:gaborbernat. (:issue:127)
v1.5.3 (2026-08-24)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index aa45dba00a6..d5155404e5f 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -235,7 +235,7 @@ pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
# via freezegun
-python-discovery==1.5.3
+python-discovery==1.6.0
# via virtualenv
python-on-whales==0.81.0
# via
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 4339b1ba6ae..92f6a039fc5 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -230,7 +230,7 @@ pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
# via freezegun
-python-discovery==1.5.3
+python-discovery==1.6.0
# via virtualenv
python-on-whales==0.81.0
# via
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 5a41f837895..faf97cf3db5 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -125,7 +125,7 @@ pytest-mock==3.15.1
# via -r requirements/lint.in
python-dateutil==2.9.0.post0
# via freezegun
-python-discovery==1.5.3
+python-discovery==1.6.0
# via virtualenv
python-on-whales==0.81.0
# via -r requirements/lint.in
From 1f47e5e88e7cbf3df97824bf64927386ac40dc94 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 1 Sep 2026 12:03:49 +0000
Subject: [PATCH 4/6] Bump cryptography from 50.0.0 to 50.0.1 (#13595)
Bumps [cryptography](https://github.com/pyca/cryptography) from 50.0.0
to 50.0.1.
Changelog
Sourced from cryptography's
changelog.
50.0.1 - 2026-08-25
* Updated Windows, macOS, and Linux wheels to be compiled with OpenSSL
4.0.2.
.. _v50-0-0:
Commits
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
6 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index d5155404e5f..696b354f504 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -69,7 +69,7 @@ coverage==7.16.0
# via
# -r requirements/test-common.in
# pytest-cov
-cryptography==50.0.0
+cryptography==50.0.1
# via trustme
cython==3.3.0
# via -r requirements/cython.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 92f6a039fc5..af29699365f 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -69,7 +69,7 @@ coverage==7.16.0
# via
# -r requirements/test-common.in
# pytest-cov
-cryptography==50.0.0
+cryptography==50.0.1
# via trustme
distlib==0.4.3
# via virtualenv
diff --git a/requirements/lint.txt b/requirements/lint.txt
index faf97cf3db5..5f2014b8431 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -34,7 +34,7 @@ cffi==2.1.1
# pycares
cfgv==3.5.0
# via pre-commit
-cryptography==50.0.0
+cryptography==50.0.1
# via trustme
distlib==0.4.3
# via virtualenv
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 7e828ff0243..7471fe9aee6 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -26,7 +26,7 @@ coverage==7.16.0
# via
# -r requirements/test-common.in
# pytest-cov
-cryptography==50.0.0
+cryptography==50.0.1
# via trustme
exceptiongroup==1.3.1
# via pytest
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index d42ce1a7c31..1df6989293e 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -42,7 +42,7 @@ coverage==7.16.0
# via
# -r requirements/test-common.in
# pytest-cov
-cryptography==50.0.0
+cryptography==50.0.1
# via trustme
exceptiongroup==1.3.1
# via
diff --git a/requirements/test.txt b/requirements/test.txt
index 87c7b97631a..cce5aaf44ff 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -42,7 +42,7 @@ coverage==7.16.0
# via
# -r requirements/test-common.in
# pytest-cov
-cryptography==50.0.0
+cryptography==50.0.1
# via trustme
exceptiongroup==1.3.1
# via
From bcddd13c6f24a1d2f1060f9505b818a53738887c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 1 Sep 2026 12:22:29 +0000
Subject: [PATCH 5/6] Bump virtualenv from 21.7.5 to 21.7.7 (#13616)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [virtualenv](https://github.com/pypa/virtualenv) from 21.7.5 to
21.7.7.
Release notes
Sourced from virtualenv's
releases.
21.7.7
What's Changed
Full Changelog: https://github.com/pypa/virtualenv/compare/21.7.6...21.7.7
21.7.6
What's Changed
Full Changelog: https://github.com/pypa/virtualenv/compare/21.7.5...21.7.6
Changelog
Sourced from virtualenv's
changelog.
Bugfixes - 21.7.7
- Bump the
python-discovery minimum to
>=1.6 for PythonInfo.system_exe, which
reports the system interpreter
without the nullable typing of system_executable - by
:user:gaborbernat. (:issue:3224)
Improved Documentation - 21.7.7
- Document the names the interpreter answers to inside a created
environment. A new :doc:
reference/environment-layout
page lists them per platform, the tutorial and the usage guide point at
it, and the explanation of creators covers why
an environment carries several names and why Windows copies a redirector
- by :user:gaborbernat. (:issue:3225)
v21.7.6 (2026-08-28)
Bugfixes - 21.7.6
- On Windows,
virtualenv no longer copies the CPython
3.13+ venvlauncher.exe shim into Scripts under
the
shim's own name, and a host such as python_d.exe gets its
alias back. The alias set took its names from the shim
that stands in for the interpreter, so every environment gained a stray
launcher copy and lost the interpreter's own
file name - by :user:darrenhuai.
(:issue:3223)
v21.7.5 (2026-08-25)
Commits
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 696b354f504..346df184588 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -331,7 +331,7 @@ uvloop==0.22.1 ; platform_system != "Windows"
# -r requirements/lint.in
valkey==6.1.1
# via -r requirements/lint.in
-virtualenv==21.7.5
+virtualenv==21.7.7
# via pre-commit
wheel==0.48.0
# via pip-tools
diff --git a/requirements/dev.txt b/requirements/dev.txt
index af29699365f..ba443755289 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -321,7 +321,7 @@ uvloop==0.22.1 ; platform_system != "Windows" and implementation_name == "cpytho
# -r requirements/lint.in
valkey==6.1.1
# via -r requirements/lint.in
-virtualenv==21.7.5
+virtualenv==21.7.7
# via pre-commit
wheel==0.48.0
# via pip-tools
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 5f2014b8431..9aedc9f8871 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -164,7 +164,7 @@ uvloop==0.22.1 ; platform_system != "Windows"
# via -r requirements/lint.in
valkey==6.1.1
# via -r requirements/lint.in
-virtualenv==21.7.5
+virtualenv==21.7.7
# via pre-commit
yarl==1.24.5
# via aiohttp
From 502dcec0728d0937fc2eacde3fd6e5133acf57ed Mon Sep 17 00:00:00 2001
From: Sam Bull
Date: Tue, 1 Sep 2026 16:29:27 +0100
Subject: [PATCH 6/6] Fix 2 flow control stalls (#13504)
---
CHANGES/13504.bugfix.rst | 1 +
aiohttp/_http_parser.pyx | 15 +++--
aiohttp/_websocket/writer.py | 2 +-
aiohttp/web_protocol.py | 122 +++++++++++++++++++++--------------
tests/test_web_functional.py | 112 ++++++++++++++++++++++++++++++++
5 files changed, 195 insertions(+), 57 deletions(-)
create mode 100644 CHANGES/13504.bugfix.rst
diff --git a/CHANGES/13504.bugfix.rst b/CHANGES/13504.bugfix.rst
new file mode 100644
index 00000000000..e3a4a73d533
--- /dev/null
+++ b/CHANGES/13504.bugfix.rst
@@ -0,0 +1 @@
+Fixed two edge cases where flow control could get stuck -- by :user:`Dreamsorcerer`.
diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx
index c78d9a30be4..25218645263 100644
--- a/aiohttp/_http_parser.pyx
+++ b/aiohttp/_http_parser.pyx
@@ -552,6 +552,9 @@ cdef class HttpParser:
self._payload = DeflateBuffer(payload, encoding, max_decompress_size=self._limit)
self._messages.append((msg, payload))
+ if self._max_msg_queue_size:
+ # Count the message where it is handed over, not where its body completes.
+ self._msg_in_flight += 1
cdef _on_message_complete(self):
# The payload is None when feed_eof() already completed a fully
@@ -940,12 +943,12 @@ cdef int cb_on_message_complete(cparser.llhttp_t* parser) except? -1:
pyparser._last_error = exc
return -1
else:
- if pyparser._max_msg_queue_size:
- pyparser._msg_in_flight += 1
- if pyparser._msg_in_flight >= pyparser._max_msg_queue_size:
- # Queue full: pause llhttp between messages. feed_data() buffers
- # the remainder as tail; resumes once the queue drains.
- return cparser.HPE_PAUSED
+ if (
+ pyparser._max_msg_queue_size
+ and pyparser._msg_in_flight >= pyparser._max_msg_queue_size
+ ):
+ # Queue full: pause llhttp between messages.
+ return cparser.HPE_PAUSED
return 0
diff --git a/aiohttp/_websocket/writer.py b/aiohttp/_websocket/writer.py
index 6bcc2867f4e..591c3d224c5 100644
--- a/aiohttp/_websocket/writer.py
+++ b/aiohttp/_websocket/writer.py
@@ -49,7 +49,7 @@ class WebSocketWriter:
def __init__(
self,
protocol: BaseProtocol,
- transport: asyncio.Transport,
+ transport: asyncio.WriteTransport,
*,
use_mask: bool = False,
limit: int = DEFAULT_CHUNK_SIZE,
diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py
index f17db73b3f5..0b591ad128d 100644
--- a/aiohttp/web_protocol.py
+++ b/aiohttp/web_protocol.py
@@ -458,6 +458,12 @@ def set_parser(
self._payload_parser = parser
self._data_received_cb = data_received_cb
+ if self._reading_paused:
+ # After upgrade nothing will read the stream again, so we need
+ # to resume here before feeding the tail, so a pause in the
+ # upgraded protocol still takes effect.
+ self.resume_reading(resume_parser=False)
+
if self._message_tail:
self._payload_parser.feed_data(self._message_tail)
self._message_tail = b""
@@ -556,6 +562,67 @@ def _resume_msg_queue_reading(self) -> None:
# ignored (see PAUSE_RESUME_READING_ERRORS; do not use suppress).
pass
+ def _replay_message_tail(self) -> None:
+ """Re-feed the bytes buffered behind a rejected upgrade.
+
+ The parser stops at an upgrade boundary and holds everything after it in
+ ``_message_tail``. If the upgrade is rejected those bytes are
+ pipelined requests and have to go back through the parser.
+ """
+ if (
+ not self._upgraded
+ # The upgrade request is the last request before the parser paused,
+ # so wait for messages to be empty.
+ or self._messages
+ # payload_parser is not None if the upgrade was accepted.
+ or self._payload_parser is not None
+ or self._parser is None
+ ):
+ return
+
+ self._parser.set_upgraded(False)
+ self._upgraded = False
+ if not self._message_tail:
+ return
+
+ messages: Sequence[_MsgType]
+ try:
+ messages, upgraded, tail = self._parser.feed_data(self._message_tail)
+ except HttpProcessingError as parse_exc:
+ # Garbage (or an oversized request line) buffered behind the
+ # upgrade: answer 400 instead of letting the error escape
+ # and lose this response, like data_received() does.
+ messages = [
+ (
+ _ErrInfo(
+ status=400,
+ exc=parse_exc,
+ message=parse_exc.message,
+ ),
+ EMPTY_PAYLOAD,
+ )
+ ]
+ upgraded = False
+ tail = b""
+
+ # A further upgrade request in the tail buffers its own remainder.
+ self._upgraded = upgraded
+ self._message_tail = tail
+ for msg, payload in messages:
+ self._request_count += 1
+ self._messages.append((msg, payload))
+
+ if len(self._messages) >= self._max_msg_queue_size:
+ # Pause the transport, like in data_received().
+ self._pause_msg_queue_reading()
+ elif self._msg_queue_paused:
+ # Resume reading now the tail has been parsed.
+ self._resume_msg_queue_reading()
+
+ # This shouldn't be possible. If a future refactor results in this
+ # failing, then the code may need to be updated to set the waiter.
+ assert self._waiter is None
+
def keep_alive(self, val: bool) -> None:
"""Set keep-alive connection mode.
@@ -789,6 +856,10 @@ async def start(self) -> None:
payload.set_exception(_PAYLOAD_ACCESS_ERROR)
+ # Draining the body above can have been what finally settled a
+ # deferred upgrade, seating a tail that finish_response() was
+ # too early to see.
+ self._replay_message_tail()
except asyncio.CancelledError:
self.log_debug("Ignored premature client disconnection")
self.force_close()
@@ -833,56 +904,7 @@ async def finish_response(
"""
request._finish()
- # Handle feeding the message tail following an upgrade request that
- # was declined.
- # The upgrade request is the last request before the parser paused,
- # so wait for self._messages to be empty.
- # payload_parser is not None if the upgrade was accepted.
- if (
- self._upgraded
- and not self._messages
- and self._payload_parser is None
- and self._parser is not None
- ):
- self._parser.set_upgraded(False)
- self._upgraded = False
- if self._message_tail:
- messages: Sequence[_MsgType]
- try:
- messages, upgraded, tail = self._parser.feed_data(
- self._message_tail
- )
- except HttpProcessingError as parse_exc:
- # Garbage (or an oversized request line) buffered behind the
- # upgrade: answer 400 instead of letting the error escape
- # and lose this response, like data_received() does.
- messages = [
- (
- _ErrInfo(
- status=400,
- exc=parse_exc,
- message=parse_exc.message,
- ),
- EMPTY_PAYLOAD,
- )
- ]
- upgraded = False
- tail = b""
- # A further upgrade request in the tail buffers its own remainder.
- self._upgraded = upgraded
- self._message_tail = tail
- for msg, payload in messages:
- self._request_count += 1
- self._messages.append((msg, payload))
- if len(self._messages) >= self._max_msg_queue_size:
- # Pause the transport, like in data_received().
- self._pause_msg_queue_reading()
- elif self._msg_queue_paused:
- # Resume reading now the tail has been parsed.
- self._resume_msg_queue_reading()
- # This shouldn't be possible. If a future refactor results in this
- # failing, then the code may need to be updated to set the waiter.
- assert self._waiter is None
+ self._replay_message_tail()
try:
prepare_meth = resp.prepare
except AttributeError:
diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py
index f129cbe90df..eaba0f79caa 100644
--- a/tests/test_web_functional.py
+++ b/tests/test_web_functional.py
@@ -2120,6 +2120,118 @@ async def upgrade_handler(request: web.Request) -> web.Response:
assert b" 400 " in response, response
+async def test_pipelined_requests_after_deferred_upgrade_are_served(
+ aiohttp_server: AiohttpServer,
+) -> None:
+ pipelined_requests = MAX_MSG_QUEUE_SIZE + 8
+ body = b"b" * 8192
+ handled: list[str] = []
+
+ async def upgrade_handler(request: web.Request) -> web.Response:
+ # Deliberately never reads request.content, so the upgrade stays pending.
+ handled.append(request.path)
+ return web.Response(text="declined")
+
+ async def plain_handler(request: web.Request) -> web.Response:
+ handled.append(request.path)
+ return web.Response(text=f"ok:{request.path}")
+
+ app = web.Application()
+ app.router.add_post("/up", upgrade_handler)
+ app.router.add_get("/{tail:.*}", plain_handler)
+ # Small enough that the unread body pauses the parser mid-message.
+ server = await aiohttp_server(app, read_bufsize=1024, lingering_time=10.0)
+
+ reader, writer = await asyncio.open_connection(server.host, server.port)
+ try:
+ writer.write(
+ b"POST /up HTTP/1.1\r\nHost: localhost\r\n"
+ b"Connection: Upgrade\r\nUpgrade: websocket\r\n"
+ b"Content-Length: "
+ + str(len(body)).encode()
+ + b"\r\n\r\n"
+ + body
+ + b"".join(
+ f"GET /r{i} HTTP/1.1\r\nHost: localhost\r\n\r\n".encode()
+ for i in range(pipelined_requests)
+ )
+ )
+ await writer.drain()
+
+ # Only ever dispatched if the drained body's tail was replayed.
+ first = await asyncio.wait_for(reader.readuntil(b"declined"), 5)
+ last = f"ok:/r{pipelined_requests - 1}".encode()
+ responses = first + await asyncio.wait_for(reader.readuntil(last), 10)
+ finally:
+ writer.close()
+ with suppress(ConnectionResetError, BrokenPipeError):
+ await writer.wait_closed()
+
+ expected = ["/up"] + [f"/r{i}" for i in range(pipelined_requests)]
+ assert handled == expected
+ # One response per request, so a duplicate dispatch cannot hide behind a
+ # readuntil() that already found what it wanted.
+ assert responses.count(b"HTTP/1.1 ") == len(expected), responses[:200]
+
+
+async def test_websocket_prepared_with_unread_body_does_not_stall(
+ aiohttp_server: AiohttpServer,
+) -> None:
+ """Upgrading with the request body unread must leave the socket readable.
+
+ The body's stream pauses reading when nobody drains it, and with the parser
+ stopped mid-message that hold is never lifted by the stream itself. Handing
+ the connection to the websocket has to release it, or the transport stays
+ paused and the websocket never receives a frame -- with keep-alive disabled
+ for the upgrade, nothing would ever reap the connection either.
+ """
+ body = b"b" * 8192
+ echoed: list[str] = []
+
+ async def ws_handler(request: web.Request) -> web.WebSocketResponse:
+ # Deliberately never reads request.content.
+ ws = web.WebSocketResponse()
+ await ws.prepare(request)
+ async for msg in ws: # pragma: no branch
+ assert isinstance(msg.data, str)
+ echoed.append(msg.data)
+ await ws.send_str(f"echo:{msg.data}")
+ break
+ return ws
+
+ app = web.Application()
+ app.router.add_post("/ws", ws_handler)
+ server = await aiohttp_server(app, read_bufsize=1024)
+
+ reader, writer = await asyncio.open_connection(server.host, server.port)
+ try:
+ writer.write(
+ b"POST /ws HTTP/1.1\r\nHost: localhost\r\n"
+ b"Connection: Upgrade\r\nUpgrade: websocket\r\n"
+ b"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
+ b"Sec-WebSocket-Version: 13\r\n"
+ b"Content-Length: " + str(len(body)).encode() + b"\r\n\r\n" + body
+ )
+ await writer.drain()
+ await asyncio.wait_for(reader.readuntil(b"\r\n\r\n"), 5)
+
+ # Sent after the handshake, so it is only read if the hold was released.
+ ws_writer = WebSocketWriter(
+ mock.Mock(_paused=False),
+ writer.transport,
+ use_mask=True,
+ )
+ await ws_writer.send_frame(b"hi", WSMsgType.TEXT)
+ await writer.drain()
+ await asyncio.wait_for(reader.readuntil(b"echo:hi"), 5)
+ finally:
+ writer.close()
+ with suppress(ConnectionResetError, BrokenPipeError):
+ await writer.wait_closed()
+
+ assert echoed == ["hi"]
+
+
async def test_declined_websocket_upgrade_reads_body(
aiohttp_server: AiohttpServer,
) -> None: