From 73e6dfdc9c6430a8e19284626cafb82f44772c50 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 26 May 2025 21:06:13 -0500 Subject: [PATCH 01/19] Increment version to 3.12.3.dev0 (#11042) --- aiohttp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp/__init__.py b/aiohttp/__init__.py index cd30c676465..4d3c8b0f2c7 100644 --- a/aiohttp/__init__.py +++ b/aiohttp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "3.12.2" +__version__ = "3.12.3.dev0" from typing import TYPE_CHECKING, Tuple From 326df2037cd8ecf578a79ffe64d02dd28736aa78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 11:14:26 +0000 Subject: [PATCH 02/19] Bump pytest-xdist from 3.6.1 to 3.7.0 (#11048) Bumps [pytest-xdist](https://github.com/pytest-dev/pytest-xdist) from 3.6.1 to 3.7.0.
Changelog

Sourced from pytest-xdist's changelog.

pytest-xdist 3.7.0 (2025-05-26)

Features

  • [#1142](https://github.com/pytest-dev/pytest-xdist/issues/1142) <https://github.com/pytest-dev/pytest-xdist/issues/1142>_: Added support for Python 3.13.

  • [#1144](https://github.com/pytest-dev/pytest-xdist/issues/1144) <https://github.com/pytest-dev/pytest-xdist/issues/1144>_: The internal steal command is now atomic - it unschedules either all requested tests or none.

    This is a prerequisite for group/scope support in the worksteal scheduler, so test groups won't be broken up incorrectly.

  • [#1170](https://github.com/pytest-dev/pytest-xdist/issues/1170) <https://github.com/pytest-dev/pytest-xdist/issues/1170>_: Add the --px arg to create proxy gateways.

    Proxy gateways are passed to additional gateways using the via keyword. They can serve as a way to run multiple workers on remote machines.

  • [#1200](https://github.com/pytest-dev/pytest-xdist/issues/1200) <https://github.com/pytest-dev/pytest-xdist/issues/1200>_: Now multiple xdist_group markers are considered when assigning tests to groups (order does not matter).

    Previously, only the last marker would assign a test to a group, but now if a test has multiple xdist_group marks applied (for example via parametrization or via fixtures), they are merged to make a new group.

Removals

  • [#1162](https://github.com/pytest-dev/pytest-xdist/issues/1162) <https://github.com/pytest-dev/pytest-xdist/issues/1162>_: Dropped support for EOL Python 3.8.

Trivial Changes

  • [#1092](https://github.com/pytest-dev/pytest-xdist/issues/1092) <https://github.com/pytest-dev/pytest-xdist/issues/1092>_: Update an error message to better indicate where users should go for more information.

  • [#1190](https://github.com/pytest-dev/pytest-xdist/issues/1190) <https://github.com/pytest-dev/pytest-xdist/issues/1190>_: Switched to using a SPDX license identifier introduced in PEP 639.

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pytest-xdist&package-manager=pip&previous-version=3.6.1&new-version=3.7.0)](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 merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@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.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 461743dbeb1..fbda27a4859 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -190,7 +190,7 @@ pytest-mock==3.14.0 # via # -r requirements/lint.in # -r requirements/test.in -pytest-xdist==3.6.1 +pytest-xdist==3.7.0 # via -r requirements/test.in python-dateutil==2.9.0.post0 # via freezegun diff --git a/requirements/dev.txt b/requirements/dev.txt index aed40b85814..8da6504e3b5 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -185,7 +185,7 @@ pytest-mock==3.14.0 # via # -r requirements/lint.in # -r requirements/test.in -pytest-xdist==3.6.1 +pytest-xdist==3.7.0 # via -r requirements/test.in python-dateutil==2.9.0.post0 # via freezegun diff --git a/requirements/test.txt b/requirements/test.txt index c567a8b0f57..34b27407189 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -104,7 +104,7 @@ pytest-cov==6.1.1 # via -r requirements/test.in pytest-mock==3.14.0 # via -r requirements/test.in -pytest-xdist==3.6.1 +pytest-xdist==3.7.0 # via -r requirements/test.in python-dateutil==2.9.0.post0 # via freezegun From d43a5e0851539a178db4a76ec2f1772ce3b2a8bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 11:23:35 +0000 Subject: [PATCH 03/19] Bump python-on-whales from 0.76.1 to 0.77.0 (#11050) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [python-on-whales](https://github.com/gabrieldemarmiesse/python-on-whales) from 0.76.1 to 0.77.0.
Release notes

Sourced from python-on-whales's releases.

v0.77.0

What's Changed

New Contributors

Full Changelog: https://github.com/gabrieldemarmiesse/python-on-whales/compare/v0.76.1...v0.77.0

Commits
  • 78f6176 Bump version to 0.77.0
  • 31151d7 Add events method to stream Docker Compose events (#676)
  • 7a6145a Rework filters to support passing multiple filters of the same type (#635)
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=python-on-whales&package-manager=pip&previous-version=0.76.1&new-version=0.77.0)](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 merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@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 +- requirements/test.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index fbda27a4859..354c5e5e179 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -194,7 +194,7 @@ pytest-xdist==3.7.0 # via -r requirements/test.in python-dateutil==2.9.0.post0 # via freezegun -python-on-whales==0.76.1 +python-on-whales==0.77.0 # via # -r requirements/lint.in # -r requirements/test.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 8da6504e3b5..a5140caca82 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -189,7 +189,7 @@ pytest-xdist==3.7.0 # via -r requirements/test.in python-dateutil==2.9.0.post0 # via freezegun -python-on-whales==0.76.1 +python-on-whales==0.77.0 # via # -r requirements/lint.in # -r requirements/test.in diff --git a/requirements/lint.txt b/requirements/lint.txt index 410643ab314..8899563b177 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -80,7 +80,7 @@ pytest-mock==3.14.0 # via -r requirements/lint.in python-dateutil==2.9.0.post0 # via freezegun -python-on-whales==0.76.1 +python-on-whales==0.77.0 # via -r requirements/lint.in pyyaml==6.0.2 # via pre-commit diff --git a/requirements/test.txt b/requirements/test.txt index 34b27407189..7e8313a60ad 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -108,7 +108,7 @@ pytest-xdist==3.7.0 # via -r requirements/test.in python-dateutil==2.9.0.post0 # via freezegun -python-on-whales==0.76.1 +python-on-whales==0.77.0 # via -r requirements/test.in re-assert==1.1.0 # via -r requirements/test.in From 2d3716b72dbc48ece32ee3d3f13eac86a28e13ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 11:28:38 +0000 Subject: [PATCH 04/19] Bump setuptools from 80.8.0 to 80.9.0 (#11051) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [setuptools](https://github.com/pypa/setuptools) from 80.8.0 to 80.9.0.
Changelog

Sourced from setuptools's changelog.

v80.9.0

Features

  • Set a deadline for the removal of pkg_resources later this year (December). (#3085)
  • Removed reliance on pkg_resources in test_wheel. (#3085)
Commits
  • 9c4d383 Bump version: 80.8.0 → 80.9.0
  • 05cb3c8 Merge pull request #5014 from pypa/debt/pkg_resources-deadline
  • 3b0bf5b Adjust ignore
  • 9c28cdf Set a deadline for the removal of pkg_resources later this year (December).
  • a3bfef9 Merge pull request #5013 from DimitriPapadopoulos/ISC
  • 64bf9d0 Enforce ruff/flake8-implicit-str-concat rules (ISC)
  • 3250c25 Fix broken link in docs (#4947)
  • 5ccf50e Merge pull request #5006 from pypa/feature/remove-more-pkg_resources
  • 134e587 Suppress nitpicky typecheck in pyright.
  • 0bf2663 Add news fragment.
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=setuptools&package-manager=pip&previous-version=80.8.0&new-version=80.9.0)](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 merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@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/doc-spelling.txt | 2 +- requirements/doc.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 354c5e5e179..52c267344ef 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -302,7 +302,7 @@ zlib-ng==0.5.1 # The following packages are considered to be unsafe in a requirements file: pip==25.1.1 # via pip-tools -setuptools==80.8.0 +setuptools==80.9.0 # via # incremental # pip-tools diff --git a/requirements/dev.txt b/requirements/dev.txt index a5140caca82..70cdbaee58e 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -293,7 +293,7 @@ zlib-ng==0.5.1 # The following packages are considered to be unsafe in a requirements file: pip==25.1.1 # via pip-tools -setuptools==80.8.0 +setuptools==80.9.0 # via # incremental # pip-tools diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index 142aa6d7edb..a23d75b97b2 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -76,5 +76,5 @@ urllib3==2.4.0 # via requests # The following packages are considered to be unsafe in a requirements file: -setuptools==80.8.0 +setuptools==80.9.0 # via incremental diff --git a/requirements/doc.txt b/requirements/doc.txt index 08f24f4175a..b31fa72a2ef 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -69,5 +69,5 @@ urllib3==2.4.0 # via requests # The following packages are considered to be unsafe in a requirements file: -setuptools==80.8.0 +setuptools==80.9.0 # via incremental From 11c6b9f566a5ca79c330e9377ace62bad7372d52 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 11:36:10 +0000 Subject: [PATCH 05/19] Bump pytest-mock from 3.14.0 to 3.14.1 (#11049) Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.14.0 to 3.14.1.
Release notes

Sourced from pytest-mock's releases.

v3.14.1

  • #503: Python 3.14 is now officially supported.
Changelog

Sourced from pytest-mock's changelog.

3.14.1 (2025-08-26)

  • [#503](https://github.com/pytest-dev/pytest-mock/issues/503) <https://github.com/pytest-dev/pytest-mock/pull/503>_: Python 3.14 is now officially supported.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pytest-mock&package-manager=pip&previous-version=3.14.0&new-version=3.14.1)](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 merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@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 +- requirements/test.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 52c267344ef..4381f614a0c 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -186,7 +186,7 @@ pytest-codspeed==3.2.0 # -r requirements/test.in pytest-cov==6.1.1 # via -r requirements/test.in -pytest-mock==3.14.0 +pytest-mock==3.14.1 # via # -r requirements/lint.in # -r requirements/test.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 70cdbaee58e..621605dd83f 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -181,7 +181,7 @@ pytest-codspeed==3.2.0 # -r requirements/test.in pytest-cov==6.1.1 # via -r requirements/test.in -pytest-mock==3.14.0 +pytest-mock==3.14.1 # via # -r requirements/lint.in # -r requirements/test.in diff --git a/requirements/lint.txt b/requirements/lint.txt index 8899563b177..be3f45bdc97 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -76,7 +76,7 @@ pytest==8.3.5 # pytest-mock pytest-codspeed==3.2.0 # via -r requirements/lint.in -pytest-mock==3.14.0 +pytest-mock==3.14.1 # via -r requirements/lint.in python-dateutil==2.9.0.post0 # via freezegun diff --git a/requirements/test.txt b/requirements/test.txt index 7e8313a60ad..fc5c4cb7044 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -102,7 +102,7 @@ pytest-codspeed==3.2.0 # via -r requirements/test.in pytest-cov==6.1.1 # via -r requirements/test.in -pytest-mock==3.14.0 +pytest-mock==3.14.1 # via -r requirements/test.in pytest-xdist==3.7.0 # via -r requirements/test.in From 3a8825b82ae4bf45c552eae6a03f3e407f639c6a Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Wed, 28 May 2025 14:01:51 -0500 Subject: [PATCH 06/19] [PR #11055/abcb2cc4 backport][3.12] Fix failing lint jobs due to caching (#11058) Co-authored-by: J. Nick Koston --- .github/workflows/ci-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index daa701c2aa9..69b777e0624 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -49,7 +49,7 @@ jobs: - name: Cache PyPI uses: actions/cache@v4.2.3 with: - key: pip-lint-${{ hashFiles('requirements/*.txt') }} + key: pip-lint-${{ hashFiles('requirements/*.txt') }}-v2 path: ~/.cache/pip restore-keys: | pip-lint- From a26ff7d57ee2e8da1c7acb73ebbdbf6564e70666 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Wed, 28 May 2025 14:02:08 -0500 Subject: [PATCH 07/19] [PR #11055/abcb2cc4 backport][3.13] Fix failing lint jobs due to caching (#11059) Co-authored-by: J. Nick Koston --- .github/workflows/ci-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index daa701c2aa9..69b777e0624 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -49,7 +49,7 @@ jobs: - name: Cache PyPI uses: actions/cache@v4.2.3 with: - key: pip-lint-${{ hashFiles('requirements/*.txt') }} + key: pip-lint-${{ hashFiles('requirements/*.txt') }}-v2 path: ~/.cache/pip restore-keys: | pip-lint- From 5708f26985d82ba171750faa58ee90c574fcddb3 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Wed, 28 May 2025 20:07:03 +0000 Subject: [PATCH 08/19] [PR #11060/59259572 backport][3.13] Fix failing linter CI (#11063) Co-authored-by: J. Nick Koston --- .github/workflows/ci-cd.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 69b777e0624..83f5fd3ee03 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -49,7 +49,7 @@ jobs: - name: Cache PyPI uses: actions/cache@v4.2.3 with: - key: pip-lint-${{ hashFiles('requirements/*.txt') }}-v2 + key: pip-lint-${{ hashFiles('requirements/*.txt') }}-v3 path: ~/.cache/pip restore-keys: | pip-lint- @@ -69,6 +69,7 @@ jobs: make mypy - name: Install libenchant run: | + sudo apt-get update sudo apt install libenchant-2-dev - name: Install spell checker run: | From 09396d030361b02c6c2145836b07cb2590ce9beb Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Wed, 28 May 2025 20:15:30 +0000 Subject: [PATCH 09/19] [PR #11060/59259572 backport][3.12] Fix failing linter CI (#11062) Co-authored-by: J. Nick Koston --- .github/workflows/ci-cd.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 69b777e0624..83f5fd3ee03 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -49,7 +49,7 @@ jobs: - name: Cache PyPI uses: actions/cache@v4.2.3 with: - key: pip-lint-${{ hashFiles('requirements/*.txt') }}-v2 + key: pip-lint-${{ hashFiles('requirements/*.txt') }}-v3 path: ~/.cache/pip restore-keys: | pip-lint- @@ -69,6 +69,7 @@ jobs: make mypy - name: Install libenchant run: | + sudo apt-get update sudo apt install libenchant-2-dev - name: Install spell checker run: | From 248ba76083367f5ae36a393053d1281960f0e2a5 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Wed, 28 May 2025 20:43:33 +0000 Subject: [PATCH 10/19] [PR #11056/7f691674 backport][3.13] Prevent blockbuster False Positives from coverage.py Locking (#11066) Co-authored-by: J. Nick Koston --- tests/conftest.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 69469b3c793..54e0d3f21a7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -80,6 +80,14 @@ def blockbuster(request: pytest.FixtureRequest) -> Iterator[None]: bb.functions[func].can_block_in( "aiohttp/web_urldispatcher.py", "add_static" ) + # Note: coverage.py uses locking internally which can cause false positives + # in blockbuster when it instruments code. This is particularly problematic + # on Windows where it can lead to flaky test failures. + # Additionally, we're not particularly worried about threading.Lock.acquire happening + # by accident in this codebase as we primarily use asyncio.Lock for + # synchronization in async code. + # Allow lock.acquire calls to prevent these false positives + bb.functions["threading.Lock.acquire"].deactivate() yield From 872cab623e818d2a19d92591d9f0cc76ac16d608 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Wed, 28 May 2025 20:47:34 +0000 Subject: [PATCH 11/19] [PR #11056/7f691674 backport][3.12] Prevent blockbuster False Positives from coverage.py Locking (#11065) Co-authored-by: J. Nick Koston --- tests/conftest.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 69469b3c793..54e0d3f21a7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -80,6 +80,14 @@ def blockbuster(request: pytest.FixtureRequest) -> Iterator[None]: bb.functions[func].can_block_in( "aiohttp/web_urldispatcher.py", "add_static" ) + # Note: coverage.py uses locking internally which can cause false positives + # in blockbuster when it instruments code. This is particularly problematic + # on Windows where it can lead to flaky test failures. + # Additionally, we're not particularly worried about threading.Lock.acquire happening + # by accident in this codebase as we primarily use asyncio.Lock for + # synchronization in async code. + # Allow lock.acquire calls to prevent these false positives + bb.functions["threading.Lock.acquire"].deactivate() yield From 146905b006e6de6bec5102f8c1a9edc0f53ac2e1 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Wed, 28 May 2025 21:10:29 +0000 Subject: [PATCH 12/19] [PR #11054/e2eb1959 backport][3.13] Fix CookieJar memory leak in filter_cookies() (#11069) Co-authored-by: J. Nick Koston Fixes #11052 memory leak issue --- CHANGES/11052.bugfix.rst | 2 ++ CHANGES/11054.bugfix.rst | 1 + aiohttp/cookiejar.py | 2 ++ tests/test_cookiejar.py | 54 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+) create mode 100644 CHANGES/11052.bugfix.rst create mode 120000 CHANGES/11054.bugfix.rst diff --git a/CHANGES/11052.bugfix.rst b/CHANGES/11052.bugfix.rst new file mode 100644 index 00000000000..73e4ea216c8 --- /dev/null +++ b/CHANGES/11052.bugfix.rst @@ -0,0 +1,2 @@ +Fixed memory leak in :py:meth:`~aiohttp.CookieJar.filter_cookies` that caused unbounded memory growth +when making requests to different URL paths -- by :user:`bdraco` and :user:`Cycloctane`. diff --git a/CHANGES/11054.bugfix.rst b/CHANGES/11054.bugfix.rst new file mode 120000 index 00000000000..2d6e2428f3e --- /dev/null +++ b/CHANGES/11054.bugfix.rst @@ -0,0 +1 @@ +11052.bugfix.rst \ No newline at end of file diff --git a/aiohttp/cookiejar.py b/aiohttp/cookiejar.py index f6b9a921767..696ffddc315 100644 --- a/aiohttp/cookiejar.py +++ b/aiohttp/cookiejar.py @@ -353,6 +353,8 @@ def filter_cookies(self, request_url: URL = URL()) -> "BaseCookie[str]": path_len = len(request_url.path) # Point 2: https://www.rfc-editor.org/rfc/rfc6265.html#section-5.4 for p in pairs: + if p not in self._cookies: + continue for name, cookie in self._cookies[p].items(): domain = cookie["domain"] diff --git a/tests/test_cookiejar.py b/tests/test_cookiejar.py index 4c37e962597..26efaa30d04 100644 --- a/tests/test_cookiejar.py +++ b/tests/test_cookiejar.py @@ -1127,3 +1127,57 @@ async def test_treat_as_secure_origin() -> None: assert len(jar) == 1 filtered_cookies = jar.filter_cookies(request_url=endpoint) assert len(filtered_cookies) == 1 + + +async def test_filter_cookies_does_not_leak_memory() -> None: + """Test that filter_cookies doesn't create empty cookie entries. + + Regression test for https://github.com/aio-libs/aiohttp/issues/11052 + """ + jar = CookieJar() + + # Set a cookie with Path=/ + jar.update_cookies({"test_cookie": "value; Path=/"}, URL("http://example.com/")) + + # Check initial state + assert len(jar) == 1 + initial_storage_size = len(jar._cookies) + initial_morsel_cache_size = len(jar._morsel_cache) + + # Make multiple requests with different paths + paths = [ + "/", + "/api", + "/api/v1", + "/api/v1/users", + "/api/v1/users/123", + "/static/css/style.css", + "/images/logo.png", + ] + + for path in paths: + url = URL(f"http://example.com{path}") + filtered = jar.filter_cookies(url) + # Should still get the cookie + assert len(filtered) == 1 + assert "test_cookie" in filtered + + # Storage size should not grow significantly + # Only the shared cookie entry ('', '') may be added + final_storage_size = len(jar._cookies) + assert final_storage_size <= initial_storage_size + 1 + + # Verify _morsel_cache doesn't leak either + # It should only have entries for domains/paths where cookies exist + final_morsel_cache_size = len(jar._morsel_cache) + assert final_morsel_cache_size <= initial_morsel_cache_size + 1 + + # Verify no empty entries were created for domain-path combinations + for key, cookies in jar._cookies.items(): + if key != ("", ""): # Skip the shared cookie entry + assert len(cookies) > 0, f"Empty cookie entry found for {key}" + + # Verify _morsel_cache entries correspond to actual cookies + for key, morsels in jar._morsel_cache.items(): + assert key in jar._cookies, f"Orphaned morsel cache entry for {key}" + assert len(morsels) > 0, f"Empty morsel cache entry found for {key}" From 696ae5275ae463fb89fc6cca3194f0b81d3513f8 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Wed, 28 May 2025 21:13:32 +0000 Subject: [PATCH 13/19] [PR #11054/e2eb1959 backport][3.12] Fix CookieJar memory leak in filter_cookies() (#11068) Co-authored-by: J. Nick Koston Fixes #11052 memory leak issue --- CHANGES/11052.bugfix.rst | 2 ++ CHANGES/11054.bugfix.rst | 1 + aiohttp/cookiejar.py | 2 ++ tests/test_cookiejar.py | 54 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+) create mode 100644 CHANGES/11052.bugfix.rst create mode 120000 CHANGES/11054.bugfix.rst diff --git a/CHANGES/11052.bugfix.rst b/CHANGES/11052.bugfix.rst new file mode 100644 index 00000000000..73e4ea216c8 --- /dev/null +++ b/CHANGES/11052.bugfix.rst @@ -0,0 +1,2 @@ +Fixed memory leak in :py:meth:`~aiohttp.CookieJar.filter_cookies` that caused unbounded memory growth +when making requests to different URL paths -- by :user:`bdraco` and :user:`Cycloctane`. diff --git a/CHANGES/11054.bugfix.rst b/CHANGES/11054.bugfix.rst new file mode 120000 index 00000000000..2d6e2428f3e --- /dev/null +++ b/CHANGES/11054.bugfix.rst @@ -0,0 +1 @@ +11052.bugfix.rst \ No newline at end of file diff --git a/aiohttp/cookiejar.py b/aiohttp/cookiejar.py index f6b9a921767..696ffddc315 100644 --- a/aiohttp/cookiejar.py +++ b/aiohttp/cookiejar.py @@ -353,6 +353,8 @@ def filter_cookies(self, request_url: URL = URL()) -> "BaseCookie[str]": path_len = len(request_url.path) # Point 2: https://www.rfc-editor.org/rfc/rfc6265.html#section-5.4 for p in pairs: + if p not in self._cookies: + continue for name, cookie in self._cookies[p].items(): domain = cookie["domain"] diff --git a/tests/test_cookiejar.py b/tests/test_cookiejar.py index 4c37e962597..26efaa30d04 100644 --- a/tests/test_cookiejar.py +++ b/tests/test_cookiejar.py @@ -1127,3 +1127,57 @@ async def test_treat_as_secure_origin() -> None: assert len(jar) == 1 filtered_cookies = jar.filter_cookies(request_url=endpoint) assert len(filtered_cookies) == 1 + + +async def test_filter_cookies_does_not_leak_memory() -> None: + """Test that filter_cookies doesn't create empty cookie entries. + + Regression test for https://github.com/aio-libs/aiohttp/issues/11052 + """ + jar = CookieJar() + + # Set a cookie with Path=/ + jar.update_cookies({"test_cookie": "value; Path=/"}, URL("http://example.com/")) + + # Check initial state + assert len(jar) == 1 + initial_storage_size = len(jar._cookies) + initial_morsel_cache_size = len(jar._morsel_cache) + + # Make multiple requests with different paths + paths = [ + "/", + "/api", + "/api/v1", + "/api/v1/users", + "/api/v1/users/123", + "/static/css/style.css", + "/images/logo.png", + ] + + for path in paths: + url = URL(f"http://example.com{path}") + filtered = jar.filter_cookies(url) + # Should still get the cookie + assert len(filtered) == 1 + assert "test_cookie" in filtered + + # Storage size should not grow significantly + # Only the shared cookie entry ('', '') may be added + final_storage_size = len(jar._cookies) + assert final_storage_size <= initial_storage_size + 1 + + # Verify _morsel_cache doesn't leak either + # It should only have entries for domains/paths where cookies exist + final_morsel_cache_size = len(jar._morsel_cache) + assert final_morsel_cache_size <= initial_morsel_cache_size + 1 + + # Verify no empty entries were created for domain-path combinations + for key, cookies in jar._cookies.items(): + if key != ("", ""): # Skip the shared cookie entry + assert len(cookies) > 0, f"Empty cookie entry found for {key}" + + # Verify _morsel_cache entries correspond to actual cookies + for key, morsels in jar._morsel_cache.items(): + assert key in jar._cookies, f"Orphaned morsel cache entry for {key}" + assert len(morsels) > 0, f"Empty morsel cache entry found for {key}" From 3df3ab5a2fb2ba3bdc016a8515e04663e8903755 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 May 2025 16:45:41 -0500 Subject: [PATCH 14/19] [PR #11064/876102c backport][3.12] Remove update of libenchant from linter workflow (#11071) --- .github/workflows/ci-cd.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 83f5fd3ee03..1d44ddda982 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -49,7 +49,7 @@ jobs: - name: Cache PyPI uses: actions/cache@v4.2.3 with: - key: pip-lint-${{ hashFiles('requirements/*.txt') }}-v3 + key: pip-lint-${{ hashFiles('requirements/*.txt') }}-v4 path: ~/.cache/pip restore-keys: | pip-lint- @@ -67,10 +67,6 @@ jobs: - name: Run linters run: | make mypy - - name: Install libenchant - run: | - sudo apt-get update - sudo apt install libenchant-2-dev - name: Install spell checker run: | pip install -r requirements/doc-spelling.in -c requirements/doc-spelling.txt From 296d50c153c18af150ac6e1be64ed678b3a17695 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 May 2025 16:50:09 -0500 Subject: [PATCH 15/19] [PR #11064/876102c backport][3.13] Remove update of libenchant from linter workflow (#11072) --- .github/workflows/ci-cd.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 83f5fd3ee03..1d44ddda982 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -49,7 +49,7 @@ jobs: - name: Cache PyPI uses: actions/cache@v4.2.3 with: - key: pip-lint-${{ hashFiles('requirements/*.txt') }}-v3 + key: pip-lint-${{ hashFiles('requirements/*.txt') }}-v4 path: ~/.cache/pip restore-keys: | pip-lint- @@ -67,10 +67,6 @@ jobs: - name: Run linters run: | make mypy - - name: Install libenchant - run: | - sudo apt-get update - sudo apt install libenchant-2-dev - name: Install spell checker run: | pip install -r requirements/doc-spelling.in -c requirements/doc-spelling.txt From 2002b9dd09436bb232e1df651ede5bb92e5f04c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 May 2025 17:20:09 -0500 Subject: [PATCH 16/19] Release 3.12.3 (#11073) --- CHANGES.rst | 19 +++++++++++++++++++ CHANGES/11052.bugfix.rst | 2 -- CHANGES/11054.bugfix.rst | 1 - aiohttp/__init__.py | 2 +- 4 files changed, 20 insertions(+), 4 deletions(-) delete mode 100644 CHANGES/11052.bugfix.rst delete mode 120000 CHANGES/11054.bugfix.rst diff --git a/CHANGES.rst b/CHANGES.rst index b0fdbe7ed5c..418475ce772 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,25 @@ .. towncrier release notes start +3.12.3 (2025-05-28) +=================== + +Bug fixes +--------- + +- Fixed memory leak in :py:meth:`~aiohttp.CookieJar.filter_cookies` that caused unbounded memory growth + when making requests to different URL paths -- by :user:`bdraco` and :user:`Cycloctane`. + + + *Related issues and pull requests on GitHub:* + :issue:`11052`, :issue:`11054`. + + + + +---- + + 3.12.2 (2025-05-26) =================== diff --git a/CHANGES/11052.bugfix.rst b/CHANGES/11052.bugfix.rst deleted file mode 100644 index 73e4ea216c8..00000000000 --- a/CHANGES/11052.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fixed memory leak in :py:meth:`~aiohttp.CookieJar.filter_cookies` that caused unbounded memory growth -when making requests to different URL paths -- by :user:`bdraco` and :user:`Cycloctane`. diff --git a/CHANGES/11054.bugfix.rst b/CHANGES/11054.bugfix.rst deleted file mode 120000 index 2d6e2428f3e..00000000000 --- a/CHANGES/11054.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -11052.bugfix.rst \ No newline at end of file diff --git a/aiohttp/__init__.py b/aiohttp/__init__.py index 4d3c8b0f2c7..31c39176b03 100644 --- a/aiohttp/__init__.py +++ b/aiohttp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "3.12.3.dev0" +__version__ = "3.12.3" from typing import TYPE_CHECKING, Tuple From e550c78a943be34ffd01b58acbcc0f06485bbbd9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 May 2025 19:01:52 -0500 Subject: [PATCH 17/19] [PR #3733/6b0bc4ef backport][3.13] Fix connector not waiting for connections to close (#11074) fixes #1925 fixes #3736 --- CHANGES/11074.bugfix.rst | 1 + CHANGES/1925.bugfix.rst | 1 + aiohttp/client_proto.py | 15 ++++++++ aiohttp/connector.py | 60 ++++++++++++++++++++++++------ tests/test_client_request.py | 7 +++- tests/test_client_session.py | 9 ++++- tests/test_connector.py | 71 +++++++++++++++++++++++++----------- 7 files changed, 130 insertions(+), 34 deletions(-) create mode 100644 CHANGES/11074.bugfix.rst create mode 120000 CHANGES/1925.bugfix.rst diff --git a/CHANGES/11074.bugfix.rst b/CHANGES/11074.bugfix.rst new file mode 100644 index 00000000000..120f8efd914 --- /dev/null +++ b/CHANGES/11074.bugfix.rst @@ -0,0 +1 @@ +Fixed connector not waiting for connections to close before returning from :meth:`~aiohttp.BaseConnector.close` (partial backport of :pr:`3733`) -- by :user:`atemate` and :user:`bdraco`. diff --git a/CHANGES/1925.bugfix.rst b/CHANGES/1925.bugfix.rst new file mode 120000 index 00000000000..eb158f4b0f9 --- /dev/null +++ b/CHANGES/1925.bugfix.rst @@ -0,0 +1 @@ +11074.bugfix.rst \ No newline at end of file diff --git a/aiohttp/client_proto.py b/aiohttp/client_proto.py index 2d64b3f3644..6a0318e553a 100644 --- a/aiohttp/client_proto.py +++ b/aiohttp/client_proto.py @@ -4,6 +4,7 @@ from .base_protocol import BaseProtocol from .client_exceptions import ( + ClientConnectionError, ClientOSError, ClientPayloadError, ServerDisconnectedError, @@ -14,6 +15,7 @@ EMPTY_BODY_STATUS_CODES, BaseTimerContext, set_exception, + set_result, ) from .http import HttpResponseParser, RawResponseMessage from .http_exceptions import HttpProcessingError @@ -43,6 +45,7 @@ def __init__(self, loop: asyncio.AbstractEventLoop) -> None: self._read_timeout_handle: Optional[asyncio.TimerHandle] = None self._timeout_ceil_threshold: Optional[float] = 5 + self.closed: asyncio.Future[None] = self._loop.create_future() @property def upgraded(self) -> bool: @@ -83,6 +86,18 @@ def connection_lost(self, exc: Optional[BaseException]) -> None: connection_closed_cleanly = original_connection_error is None + if connection_closed_cleanly: + set_result(self.closed, None) + else: + assert original_connection_error is not None + set_exception( + self.closed, + ClientConnectionError( + f"Connection lost: {original_connection_error !s}", + ), + original_connection_error, + ) + if self._payload_parser is not None: with suppress(Exception): # FIXME: log this somehow? self._payload_parser.feed_eof() diff --git a/aiohttp/connector.py b/aiohttp/connector.py index dd0d27a7054..926a62684f6 100644 --- a/aiohttp/connector.py +++ b/aiohttp/connector.py @@ -1,5 +1,6 @@ import asyncio import functools +import logging import random import socket import sys @@ -131,6 +132,14 @@ def __del__(self) -> None: ) +async def _wait_for_close(waiters: List[Awaitable[object]]) -> None: + """Wait for all waiters to finish closing.""" + results = await asyncio.gather(*waiters, return_exceptions=True) + for res in results: + if isinstance(res, Exception): + logging.error("Error while closing connector: %r", res) + + class Connection: _source_traceback = None @@ -222,10 +231,14 @@ def closed(self) -> bool: class _TransportPlaceholder: """placeholder for BaseConnector.connect function""" - __slots__ = () + __slots__ = ("closed",) + + def __init__(self, closed_future: asyncio.Future[Optional[Exception]]) -> None: + """Initialize a placeholder for a transport.""" + self.closed = closed_future def close(self) -> None: - """Close the placeholder transport.""" + """Close the placeholder.""" class BaseConnector: @@ -322,6 +335,10 @@ def __init__( self._cleanup_closed_disabled = not enable_cleanup_closed self._cleanup_closed_transports: List[Optional[asyncio.Transport]] = [] + self._placeholder_future: asyncio.Future[Optional[Exception]] = ( + loop.create_future() + ) + self._placeholder_future.set_result(None) self._cleanup_closed() def __del__(self, _warnings: Any = warnings) -> None: @@ -454,18 +471,30 @@ def _cleanup_closed(self) -> None: def close(self) -> Awaitable[None]: """Close all opened transports.""" - self._close() - return _DeprecationWaiter(noop()) + if not (waiters := self._close()): + # If there are no connections to close, we can return a noop + # awaitable to avoid scheduling a task on the event loop. + return _DeprecationWaiter(noop()) + coro = _wait_for_close(waiters) + if sys.version_info >= (3, 12): + # Optimization for Python 3.12, try to close connections + # immediately to avoid having to schedule the task on the event loop. + task = asyncio.Task(coro, loop=self._loop, eager_start=True) + else: + task = self._loop.create_task(coro) + return _DeprecationWaiter(task) + + def _close(self) -> List[Awaitable[object]]: + waiters: List[Awaitable[object]] = [] - def _close(self) -> None: if self._closed: - return + return waiters self._closed = True try: if self._loop.is_closed(): - return + return waiters # cancel cleanup task if self._cleanup_handle: @@ -476,16 +505,20 @@ def _close(self) -> None: self._cleanup_closed_handle.cancel() for data in self._conns.values(): - for proto, t0 in data: + for proto, _ in data: proto.close() + waiters.append(proto.closed) for proto in self._acquired: proto.close() + waiters.append(proto.closed) for transport in self._cleanup_closed_transports: if transport is not None: transport.abort() + return waiters + finally: self._conns.clear() self._acquired.clear() @@ -546,7 +579,9 @@ async def connect( if (conn := await self._get(key, traces)) is not None: return conn - placeholder = cast(ResponseHandler, _TransportPlaceholder()) + placeholder = cast( + ResponseHandler, _TransportPlaceholder(self._placeholder_future) + ) self._acquired.add(placeholder) if self._limit_per_host: self._acquired_per_host[key].add(placeholder) @@ -898,15 +933,18 @@ def __init__( self._resolve_host_tasks: Set["asyncio.Task[List[ResolveResult]]"] = set() self._socket_factory = socket_factory - def close(self) -> Awaitable[None]: + def _close(self) -> List[Awaitable[object]]: """Close all ongoing DNS calls.""" for fut in chain.from_iterable(self._throttle_dns_futures.values()): fut.cancel() + waiters = super()._close() + for t in self._resolve_host_tasks: t.cancel() + waiters.append(t) - return super().close() + return waiters @property def family(self) -> int: diff --git a/tests/test_client_request.py b/tests/test_client_request.py index 7274420d246..b3eb55d921b 100644 --- a/tests/test_client_request.py +++ b/tests/test_client_request.py @@ -69,6 +69,8 @@ def protocol(loop, transport): protocol.transport = transport protocol._drain_helper.return_value = loop.create_future() protocol._drain_helper.return_value.set_result(None) + protocol.closed = loop.create_future() + protocol.closed.set_result(None) return protocol @@ -1404,7 +1406,10 @@ async def send(self, conn): async def create_connection(req, traces, timeout): assert isinstance(req, CustomRequest) - return mock.Mock() + proto = mock.Mock() + proto.closed = loop.create_future() + proto.closed.set_result(None) + return proto connector = BaseConnector(loop=loop) connector._create_connection = create_connection diff --git a/tests/test_client_session.py b/tests/test_client_session.py index e31144abd0b..56c7a5c0c13 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -33,6 +33,8 @@ async def make_conn(): conn = loop.run_until_complete(make_conn()) proto = mock.Mock() + proto.closed = loop.create_future() + proto.closed.set_result(None) conn._conns["a"] = deque([(proto, 123)]) yield conn loop.run_until_complete(conn.close()) @@ -429,7 +431,10 @@ async def test_reraise_os_error(create_session) -> None: async def create_connection(req, traces, timeout): # return self.transport, self.protocol - return mock.Mock() + proto = mock.Mock() + proto.closed = session._loop.create_future() + proto.closed.set_result(None) + return proto session._connector._create_connection = create_connection session._connector._release = mock.Mock() @@ -464,6 +469,8 @@ async def connect(req, traces, timeout): async def create_connection(req, traces, timeout): # return self.transport, self.protocol conn = mock.Mock() + conn.closed = session._loop.create_future() + conn.closed.set_result(None) return conn session._connector.connect = connect diff --git a/tests/test_connector.py b/tests/test_connector.py index 8128b47f02d..f17ded6d960 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -293,7 +293,7 @@ async def test_async_context_manager(loop) -> None: async def test_close(loop) -> None: - proto = mock.Mock() + proto = create_mocked_conn() conn = aiohttp.BaseConnector(loop=loop) assert not conn.closed @@ -305,6 +305,35 @@ async def test_close(loop) -> None: assert conn.closed +async def test_close_with_exception_during_closing( + loop: asyncio.AbstractEventLoop, caplog: pytest.LogCaptureFixture +) -> None: + """Test that exceptions during connection closing are logged.""" + proto = create_mocked_conn() + + # Make the closed future raise an exception when awaited + exc_future = loop.create_future() + exc_future.set_exception(RuntimeError("Connection close failed")) + proto.closed = exc_future + + conn = aiohttp.BaseConnector(loop=loop) + conn._conns[("host", 8080, False)] = deque([(proto, object())]) + + # Clear any existing log records + caplog.clear() + + # Close should complete even with the exception + await conn.close() + + # Check that the error was logged + assert len(caplog.records) == 1 + assert caplog.records[0].levelname == "ERROR" + assert "Error while closing connector" in caplog.records[0].message + assert "RuntimeError('Connection close failed')" in caplog.records[0].message + + assert conn.closed + + async def test_get(loop: asyncio.AbstractEventLoop, key: ConnectionKey) -> None: conn = aiohttp.BaseConnector() try: @@ -431,7 +460,7 @@ async def test_release(loop, key) -> None: conn = aiohttp.BaseConnector(loop=loop) conn._release_waiter = mock.Mock() - proto = mock.Mock(should_close=False) + proto = create_mocked_conn(should_close=False) conn._acquired.add(proto) conn._acquired_per_host[key].add(proto) @@ -469,7 +498,7 @@ async def test_release_ssl_transport( async def test_release_already_closed(loop) -> None: conn = aiohttp.BaseConnector(loop=loop) - proto = mock.Mock() + proto = create_mocked_conn() key = 1 conn._acquired.add(proto) await conn.close() @@ -569,7 +598,7 @@ async def test_release_waiter_no_available(loop, key, key2) -> None: async def test_release_close(loop, key) -> None: conn = aiohttp.BaseConnector(loop=loop) - proto = mock.Mock(should_close=True) + proto = create_mocked_conn(should_close=True) conn._acquired.add(proto) conn._release(key, proto) @@ -1504,7 +1533,7 @@ async def test_release_close_do_not_add_to_pool(loop, key) -> None: # see issue #473 conn = aiohttp.BaseConnector(loop=loop) - proto = mock.Mock(should_close=True) + proto = create_mocked_conn(should_close=True) conn._acquired.add(proto) conn._release(key, proto) @@ -1514,12 +1543,12 @@ async def test_release_close_do_not_add_to_pool(loop, key) -> None: async def test_release_close_do_not_delete_existing_connections(key) -> None: - proto1 = mock.Mock() + proto1 = create_mocked_conn() conn = aiohttp.BaseConnector() conn._conns[key] = deque([(proto1, 1)]) - proto = mock.Mock(should_close=True) + proto = create_mocked_conn(should_close=True) conn._acquired.add(proto) conn._release(key, proto) assert conn._conns[key] == deque([(proto1, 1)]) @@ -1529,7 +1558,7 @@ async def test_release_close_do_not_delete_existing_connections(key) -> None: async def test_release_not_started(loop) -> None: conn = aiohttp.BaseConnector(loop=loop) - proto = mock.Mock(should_close=False) + proto = create_mocked_conn(should_close=False) key = 1 conn._acquired.add(proto) conn._release(key, proto) @@ -1544,7 +1573,7 @@ async def test_release_not_started(loop) -> None: async def test_release_not_opened(loop, key) -> None: conn = aiohttp.BaseConnector(loop=loop) - proto = mock.Mock() + proto = create_mocked_conn() conn._acquired.add(proto) conn._release(key, proto) assert proto.close.called @@ -1553,7 +1582,7 @@ async def test_release_not_opened(loop, key) -> None: async def test_connect(loop, key) -> None: - proto = mock.Mock() + proto = create_mocked_conn() proto.is_connected.return_value = True req = ClientRequest("GET", URL("http://localhost:80"), loop=loop) @@ -1588,7 +1617,7 @@ async def test_connect_tracing(loop) -> None: trace_config.freeze() traces = [Trace(session, trace_config, trace_config.trace_config_ctx())] - proto = mock.Mock() + proto = create_mocked_conn() proto.is_connected.return_value = True req = ClientRequest("GET", URL("http://host:80"), loop=loop) @@ -2116,7 +2145,7 @@ async def test_ssl_context_once() -> None: async def test_close_twice(loop) -> None: - proto = mock.Mock() + proto = create_mocked_conn() conn = aiohttp.BaseConnector(loop=loop) conn._conns[1] = deque([(proto, object())]) @@ -2133,7 +2162,7 @@ async def test_close_twice(loop) -> None: async def test_close_cancels_cleanup_handle(loop) -> None: conn = aiohttp.BaseConnector(loop=loop) - conn._release(1, mock.Mock(should_close=False)) + conn._release(1, create_mocked_conn(should_close=False)) assert conn._cleanup_handle is not None await conn.close() assert conn._cleanup_handle is None @@ -2584,7 +2613,7 @@ async def test_connect_queued_operation_tracing(loop, key) -> None: trace_config.freeze() traces = [Trace(session, trace_config, trace_config.trace_config_ctx())] - proto = mock.Mock() + proto = create_mocked_conn() proto.is_connected.return_value = True req = ClientRequest( @@ -2628,7 +2657,7 @@ async def test_connect_reuseconn_tracing(loop, key) -> None: trace_config.freeze() traces = [Trace(session, trace_config, trace_config.trace_config_ctx())] - proto = mock.Mock() + proto = create_mocked_conn() proto.is_connected.return_value = True req = ClientRequest( @@ -2681,7 +2710,7 @@ async def f(): async def test_connect_with_no_limit_and_limit_per_host(loop, key) -> None: - proto = mock.Mock() + proto = create_mocked_conn() proto.is_connected.return_value = True req = ClientRequest("GET", URL("http://localhost1:80"), loop=loop) @@ -2746,7 +2775,7 @@ async def f(): async def test_connect_with_limit_cancelled(loop) -> None: - proto = mock.Mock() + proto = create_mocked_conn() proto.is_connected.return_value = True req = ClientRequest("GET", URL("http://host:80"), loop=loop) @@ -2794,7 +2823,7 @@ async def check_with_exc(err: Exception) -> None: async def test_connect_with_limit_concurrent(loop) -> None: - proto = mock.Mock() + proto = create_mocked_conn() proto.should_close = False proto.is_connected.return_value = True @@ -2816,7 +2845,7 @@ async def create_connection(req, traces, timeout): # Make a new transport mock each time because acquired # transports are stored in a set. Reusing the same object # messes with the count. - proto = mock.Mock(should_close=False) + proto = create_mocked_conn(should_close=False) proto.is_connected.return_value = True return proto @@ -2899,7 +2928,7 @@ async def test_connect_waiters_cleanup_key_error(loop) -> None: async def test_close_with_acquired_connection(loop) -> None: - proto = mock.Mock() + proto = create_mocked_conn() proto.is_connected.return_value = True req = ClientRequest("GET", URL("http://host:80"), loop=loop) @@ -3017,7 +3046,7 @@ async def test_cancelled_waiter(loop) -> None: conn = aiohttp.BaseConnector(limit=1, loop=loop) req = mock.Mock() req.connection_key = "key" - proto = mock.Mock() + proto = create_mocked_conn() async def create_connection(req, traces=None): await asyncio.sleep(1) From 0abffd5f0e2e7607a505562b4592f9a00511c6c8 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Thu, 29 May 2025 00:24:39 +0000 Subject: [PATCH 18/19] [PR #11074/e550c78a backport][3.12] Fix connector not waiting for connections to close (#11077) Co-authored-by: J. Nick Koston fixes #1925 fixes #3736 --- CHANGES/11074.bugfix.rst | 1 + CHANGES/1925.bugfix.rst | 1 + aiohttp/client_proto.py | 15 ++++++++ aiohttp/connector.py | 60 ++++++++++++++++++++++++------ tests/test_client_request.py | 7 +++- tests/test_client_session.py | 9 ++++- tests/test_connector.py | 71 +++++++++++++++++++++++++----------- 7 files changed, 130 insertions(+), 34 deletions(-) create mode 100644 CHANGES/11074.bugfix.rst create mode 120000 CHANGES/1925.bugfix.rst diff --git a/CHANGES/11074.bugfix.rst b/CHANGES/11074.bugfix.rst new file mode 100644 index 00000000000..120f8efd914 --- /dev/null +++ b/CHANGES/11074.bugfix.rst @@ -0,0 +1 @@ +Fixed connector not waiting for connections to close before returning from :meth:`~aiohttp.BaseConnector.close` (partial backport of :pr:`3733`) -- by :user:`atemate` and :user:`bdraco`. diff --git a/CHANGES/1925.bugfix.rst b/CHANGES/1925.bugfix.rst new file mode 120000 index 00000000000..eb158f4b0f9 --- /dev/null +++ b/CHANGES/1925.bugfix.rst @@ -0,0 +1 @@ +11074.bugfix.rst \ No newline at end of file diff --git a/aiohttp/client_proto.py b/aiohttp/client_proto.py index 2d64b3f3644..6a0318e553a 100644 --- a/aiohttp/client_proto.py +++ b/aiohttp/client_proto.py @@ -4,6 +4,7 @@ from .base_protocol import BaseProtocol from .client_exceptions import ( + ClientConnectionError, ClientOSError, ClientPayloadError, ServerDisconnectedError, @@ -14,6 +15,7 @@ EMPTY_BODY_STATUS_CODES, BaseTimerContext, set_exception, + set_result, ) from .http import HttpResponseParser, RawResponseMessage from .http_exceptions import HttpProcessingError @@ -43,6 +45,7 @@ def __init__(self, loop: asyncio.AbstractEventLoop) -> None: self._read_timeout_handle: Optional[asyncio.TimerHandle] = None self._timeout_ceil_threshold: Optional[float] = 5 + self.closed: asyncio.Future[None] = self._loop.create_future() @property def upgraded(self) -> bool: @@ -83,6 +86,18 @@ def connection_lost(self, exc: Optional[BaseException]) -> None: connection_closed_cleanly = original_connection_error is None + if connection_closed_cleanly: + set_result(self.closed, None) + else: + assert original_connection_error is not None + set_exception( + self.closed, + ClientConnectionError( + f"Connection lost: {original_connection_error !s}", + ), + original_connection_error, + ) + if self._payload_parser is not None: with suppress(Exception): # FIXME: log this somehow? self._payload_parser.feed_eof() diff --git a/aiohttp/connector.py b/aiohttp/connector.py index dd0d27a7054..926a62684f6 100644 --- a/aiohttp/connector.py +++ b/aiohttp/connector.py @@ -1,5 +1,6 @@ import asyncio import functools +import logging import random import socket import sys @@ -131,6 +132,14 @@ def __del__(self) -> None: ) +async def _wait_for_close(waiters: List[Awaitable[object]]) -> None: + """Wait for all waiters to finish closing.""" + results = await asyncio.gather(*waiters, return_exceptions=True) + for res in results: + if isinstance(res, Exception): + logging.error("Error while closing connector: %r", res) + + class Connection: _source_traceback = None @@ -222,10 +231,14 @@ def closed(self) -> bool: class _TransportPlaceholder: """placeholder for BaseConnector.connect function""" - __slots__ = () + __slots__ = ("closed",) + + def __init__(self, closed_future: asyncio.Future[Optional[Exception]]) -> None: + """Initialize a placeholder for a transport.""" + self.closed = closed_future def close(self) -> None: - """Close the placeholder transport.""" + """Close the placeholder.""" class BaseConnector: @@ -322,6 +335,10 @@ def __init__( self._cleanup_closed_disabled = not enable_cleanup_closed self._cleanup_closed_transports: List[Optional[asyncio.Transport]] = [] + self._placeholder_future: asyncio.Future[Optional[Exception]] = ( + loop.create_future() + ) + self._placeholder_future.set_result(None) self._cleanup_closed() def __del__(self, _warnings: Any = warnings) -> None: @@ -454,18 +471,30 @@ def _cleanup_closed(self) -> None: def close(self) -> Awaitable[None]: """Close all opened transports.""" - self._close() - return _DeprecationWaiter(noop()) + if not (waiters := self._close()): + # If there are no connections to close, we can return a noop + # awaitable to avoid scheduling a task on the event loop. + return _DeprecationWaiter(noop()) + coro = _wait_for_close(waiters) + if sys.version_info >= (3, 12): + # Optimization for Python 3.12, try to close connections + # immediately to avoid having to schedule the task on the event loop. + task = asyncio.Task(coro, loop=self._loop, eager_start=True) + else: + task = self._loop.create_task(coro) + return _DeprecationWaiter(task) + + def _close(self) -> List[Awaitable[object]]: + waiters: List[Awaitable[object]] = [] - def _close(self) -> None: if self._closed: - return + return waiters self._closed = True try: if self._loop.is_closed(): - return + return waiters # cancel cleanup task if self._cleanup_handle: @@ -476,16 +505,20 @@ def _close(self) -> None: self._cleanup_closed_handle.cancel() for data in self._conns.values(): - for proto, t0 in data: + for proto, _ in data: proto.close() + waiters.append(proto.closed) for proto in self._acquired: proto.close() + waiters.append(proto.closed) for transport in self._cleanup_closed_transports: if transport is not None: transport.abort() + return waiters + finally: self._conns.clear() self._acquired.clear() @@ -546,7 +579,9 @@ async def connect( if (conn := await self._get(key, traces)) is not None: return conn - placeholder = cast(ResponseHandler, _TransportPlaceholder()) + placeholder = cast( + ResponseHandler, _TransportPlaceholder(self._placeholder_future) + ) self._acquired.add(placeholder) if self._limit_per_host: self._acquired_per_host[key].add(placeholder) @@ -898,15 +933,18 @@ def __init__( self._resolve_host_tasks: Set["asyncio.Task[List[ResolveResult]]"] = set() self._socket_factory = socket_factory - def close(self) -> Awaitable[None]: + def _close(self) -> List[Awaitable[object]]: """Close all ongoing DNS calls.""" for fut in chain.from_iterable(self._throttle_dns_futures.values()): fut.cancel() + waiters = super()._close() + for t in self._resolve_host_tasks: t.cancel() + waiters.append(t) - return super().close() + return waiters @property def family(self) -> int: diff --git a/tests/test_client_request.py b/tests/test_client_request.py index 7274420d246..b3eb55d921b 100644 --- a/tests/test_client_request.py +++ b/tests/test_client_request.py @@ -69,6 +69,8 @@ def protocol(loop, transport): protocol.transport = transport protocol._drain_helper.return_value = loop.create_future() protocol._drain_helper.return_value.set_result(None) + protocol.closed = loop.create_future() + protocol.closed.set_result(None) return protocol @@ -1404,7 +1406,10 @@ async def send(self, conn): async def create_connection(req, traces, timeout): assert isinstance(req, CustomRequest) - return mock.Mock() + proto = mock.Mock() + proto.closed = loop.create_future() + proto.closed.set_result(None) + return proto connector = BaseConnector(loop=loop) connector._create_connection = create_connection diff --git a/tests/test_client_session.py b/tests/test_client_session.py index e31144abd0b..56c7a5c0c13 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -33,6 +33,8 @@ async def make_conn(): conn = loop.run_until_complete(make_conn()) proto = mock.Mock() + proto.closed = loop.create_future() + proto.closed.set_result(None) conn._conns["a"] = deque([(proto, 123)]) yield conn loop.run_until_complete(conn.close()) @@ -429,7 +431,10 @@ async def test_reraise_os_error(create_session) -> None: async def create_connection(req, traces, timeout): # return self.transport, self.protocol - return mock.Mock() + proto = mock.Mock() + proto.closed = session._loop.create_future() + proto.closed.set_result(None) + return proto session._connector._create_connection = create_connection session._connector._release = mock.Mock() @@ -464,6 +469,8 @@ async def connect(req, traces, timeout): async def create_connection(req, traces, timeout): # return self.transport, self.protocol conn = mock.Mock() + conn.closed = session._loop.create_future() + conn.closed.set_result(None) return conn session._connector.connect = connect diff --git a/tests/test_connector.py b/tests/test_connector.py index 8128b47f02d..f17ded6d960 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -293,7 +293,7 @@ async def test_async_context_manager(loop) -> None: async def test_close(loop) -> None: - proto = mock.Mock() + proto = create_mocked_conn() conn = aiohttp.BaseConnector(loop=loop) assert not conn.closed @@ -305,6 +305,35 @@ async def test_close(loop) -> None: assert conn.closed +async def test_close_with_exception_during_closing( + loop: asyncio.AbstractEventLoop, caplog: pytest.LogCaptureFixture +) -> None: + """Test that exceptions during connection closing are logged.""" + proto = create_mocked_conn() + + # Make the closed future raise an exception when awaited + exc_future = loop.create_future() + exc_future.set_exception(RuntimeError("Connection close failed")) + proto.closed = exc_future + + conn = aiohttp.BaseConnector(loop=loop) + conn._conns[("host", 8080, False)] = deque([(proto, object())]) + + # Clear any existing log records + caplog.clear() + + # Close should complete even with the exception + await conn.close() + + # Check that the error was logged + assert len(caplog.records) == 1 + assert caplog.records[0].levelname == "ERROR" + assert "Error while closing connector" in caplog.records[0].message + assert "RuntimeError('Connection close failed')" in caplog.records[0].message + + assert conn.closed + + async def test_get(loop: asyncio.AbstractEventLoop, key: ConnectionKey) -> None: conn = aiohttp.BaseConnector() try: @@ -431,7 +460,7 @@ async def test_release(loop, key) -> None: conn = aiohttp.BaseConnector(loop=loop) conn._release_waiter = mock.Mock() - proto = mock.Mock(should_close=False) + proto = create_mocked_conn(should_close=False) conn._acquired.add(proto) conn._acquired_per_host[key].add(proto) @@ -469,7 +498,7 @@ async def test_release_ssl_transport( async def test_release_already_closed(loop) -> None: conn = aiohttp.BaseConnector(loop=loop) - proto = mock.Mock() + proto = create_mocked_conn() key = 1 conn._acquired.add(proto) await conn.close() @@ -569,7 +598,7 @@ async def test_release_waiter_no_available(loop, key, key2) -> None: async def test_release_close(loop, key) -> None: conn = aiohttp.BaseConnector(loop=loop) - proto = mock.Mock(should_close=True) + proto = create_mocked_conn(should_close=True) conn._acquired.add(proto) conn._release(key, proto) @@ -1504,7 +1533,7 @@ async def test_release_close_do_not_add_to_pool(loop, key) -> None: # see issue #473 conn = aiohttp.BaseConnector(loop=loop) - proto = mock.Mock(should_close=True) + proto = create_mocked_conn(should_close=True) conn._acquired.add(proto) conn._release(key, proto) @@ -1514,12 +1543,12 @@ async def test_release_close_do_not_add_to_pool(loop, key) -> None: async def test_release_close_do_not_delete_existing_connections(key) -> None: - proto1 = mock.Mock() + proto1 = create_mocked_conn() conn = aiohttp.BaseConnector() conn._conns[key] = deque([(proto1, 1)]) - proto = mock.Mock(should_close=True) + proto = create_mocked_conn(should_close=True) conn._acquired.add(proto) conn._release(key, proto) assert conn._conns[key] == deque([(proto1, 1)]) @@ -1529,7 +1558,7 @@ async def test_release_close_do_not_delete_existing_connections(key) -> None: async def test_release_not_started(loop) -> None: conn = aiohttp.BaseConnector(loop=loop) - proto = mock.Mock(should_close=False) + proto = create_mocked_conn(should_close=False) key = 1 conn._acquired.add(proto) conn._release(key, proto) @@ -1544,7 +1573,7 @@ async def test_release_not_started(loop) -> None: async def test_release_not_opened(loop, key) -> None: conn = aiohttp.BaseConnector(loop=loop) - proto = mock.Mock() + proto = create_mocked_conn() conn._acquired.add(proto) conn._release(key, proto) assert proto.close.called @@ -1553,7 +1582,7 @@ async def test_release_not_opened(loop, key) -> None: async def test_connect(loop, key) -> None: - proto = mock.Mock() + proto = create_mocked_conn() proto.is_connected.return_value = True req = ClientRequest("GET", URL("http://localhost:80"), loop=loop) @@ -1588,7 +1617,7 @@ async def test_connect_tracing(loop) -> None: trace_config.freeze() traces = [Trace(session, trace_config, trace_config.trace_config_ctx())] - proto = mock.Mock() + proto = create_mocked_conn() proto.is_connected.return_value = True req = ClientRequest("GET", URL("http://host:80"), loop=loop) @@ -2116,7 +2145,7 @@ async def test_ssl_context_once() -> None: async def test_close_twice(loop) -> None: - proto = mock.Mock() + proto = create_mocked_conn() conn = aiohttp.BaseConnector(loop=loop) conn._conns[1] = deque([(proto, object())]) @@ -2133,7 +2162,7 @@ async def test_close_twice(loop) -> None: async def test_close_cancels_cleanup_handle(loop) -> None: conn = aiohttp.BaseConnector(loop=loop) - conn._release(1, mock.Mock(should_close=False)) + conn._release(1, create_mocked_conn(should_close=False)) assert conn._cleanup_handle is not None await conn.close() assert conn._cleanup_handle is None @@ -2584,7 +2613,7 @@ async def test_connect_queued_operation_tracing(loop, key) -> None: trace_config.freeze() traces = [Trace(session, trace_config, trace_config.trace_config_ctx())] - proto = mock.Mock() + proto = create_mocked_conn() proto.is_connected.return_value = True req = ClientRequest( @@ -2628,7 +2657,7 @@ async def test_connect_reuseconn_tracing(loop, key) -> None: trace_config.freeze() traces = [Trace(session, trace_config, trace_config.trace_config_ctx())] - proto = mock.Mock() + proto = create_mocked_conn() proto.is_connected.return_value = True req = ClientRequest( @@ -2681,7 +2710,7 @@ async def f(): async def test_connect_with_no_limit_and_limit_per_host(loop, key) -> None: - proto = mock.Mock() + proto = create_mocked_conn() proto.is_connected.return_value = True req = ClientRequest("GET", URL("http://localhost1:80"), loop=loop) @@ -2746,7 +2775,7 @@ async def f(): async def test_connect_with_limit_cancelled(loop) -> None: - proto = mock.Mock() + proto = create_mocked_conn() proto.is_connected.return_value = True req = ClientRequest("GET", URL("http://host:80"), loop=loop) @@ -2794,7 +2823,7 @@ async def check_with_exc(err: Exception) -> None: async def test_connect_with_limit_concurrent(loop) -> None: - proto = mock.Mock() + proto = create_mocked_conn() proto.should_close = False proto.is_connected.return_value = True @@ -2816,7 +2845,7 @@ async def create_connection(req, traces, timeout): # Make a new transport mock each time because acquired # transports are stored in a set. Reusing the same object # messes with the count. - proto = mock.Mock(should_close=False) + proto = create_mocked_conn(should_close=False) proto.is_connected.return_value = True return proto @@ -2899,7 +2928,7 @@ async def test_connect_waiters_cleanup_key_error(loop) -> None: async def test_close_with_acquired_connection(loop) -> None: - proto = mock.Mock() + proto = create_mocked_conn() proto.is_connected.return_value = True req = ClientRequest("GET", URL("http://host:80"), loop=loop) @@ -3017,7 +3046,7 @@ async def test_cancelled_waiter(loop) -> None: conn = aiohttp.BaseConnector(limit=1, loop=loop) req = mock.Mock() req.connection_key = "key" - proto = mock.Mock() + proto = create_mocked_conn() async def create_connection(req, traces=None): await asyncio.sleep(1) From faa742b423cb5ae8fedb3424d0c35860052e06d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 May 2025 19:55:02 -0500 Subject: [PATCH 19/19] Release 3.12.4 (#11078) --- CHANGES.rst | 18 ++++++++++++++++++ CHANGES/11074.bugfix.rst | 1 - CHANGES/1925.bugfix.rst | 1 - aiohttp/__init__.py | 2 +- 4 files changed, 19 insertions(+), 3 deletions(-) delete mode 100644 CHANGES/11074.bugfix.rst delete mode 120000 CHANGES/1925.bugfix.rst diff --git a/CHANGES.rst b/CHANGES.rst index 418475ce772..8d3bcbac867 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,24 @@ .. towncrier release notes start +3.12.4 (2025-05-28) +=================== + +Bug fixes +--------- + +- Fixed connector not waiting for connections to close before returning from :meth:`~aiohttp.BaseConnector.close` (partial backport of :pr:`3733`) -- by :user:`atemate` and :user:`bdraco`. + + + *Related issues and pull requests on GitHub:* + :issue:`1925`, :issue:`11074`. + + + + +---- + + 3.12.3 (2025-05-28) =================== diff --git a/CHANGES/11074.bugfix.rst b/CHANGES/11074.bugfix.rst deleted file mode 100644 index 120f8efd914..00000000000 --- a/CHANGES/11074.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed connector not waiting for connections to close before returning from :meth:`~aiohttp.BaseConnector.close` (partial backport of :pr:`3733`) -- by :user:`atemate` and :user:`bdraco`. diff --git a/CHANGES/1925.bugfix.rst b/CHANGES/1925.bugfix.rst deleted file mode 120000 index eb158f4b0f9..00000000000 --- a/CHANGES/1925.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -11074.bugfix.rst \ No newline at end of file diff --git a/aiohttp/__init__.py b/aiohttp/__init__.py index 31c39176b03..56201805d30 100644 --- a/aiohttp/__init__.py +++ b/aiohttp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "3.12.3" +__version__ = "3.12.4" from typing import TYPE_CHECKING, Tuple