From 83a85fa69d82966bbdbd6bb79d24fa74ede6bbd6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2026 12:07:33 -1000 Subject: [PATCH 001/191] Incremnet version to 3.13.5.dev0 --- aiohttp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp/__init__.py b/aiohttp/__init__.py index 404f9267748..bc8bcea47ed 100644 --- a/aiohttp/__init__.py +++ b/aiohttp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "3.13.5" +__version__ = "3.13.5.dev0" from typing import TYPE_CHECKING, Tuple From 5b8c540dfcbd473de9f7f18f499561c0cb8e09c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 10:51:35 +0000 Subject: [PATCH 002/191] Bump pypa/cibuildwheel from 3.4.0 to 3.4.1 (#12314) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 3.4.0 to 3.4.1.
Release notes

Sourced from pypa/cibuildwheel's releases.

v3.4.1

  • ⚠️ Building for the experimental CPython 3.13 free-threading variant is now deprecated. That functionality will be removed in the next minor release. The enable option cpython-freethreading is therefore also deprecated. Builds specifying enable = "all" no longer select cpython-freethreading. CPython 3.14 free-threading support remains available without the enable flag. (#2787)
  • 🐛 iOS builds will no longer skip repair-wheel-command if it's defined in config (#2761)
  • 🐛 Fix bug causing uv to fail when environments define PYTHON_VERSION or UV_PYTHON, conflicting with our venvs (#2795)
  • ✨ cibuildwheel prints the selected build identifiers at the start of the build. (#2785)
  • 🔐 The GitHub Action now references other actions with a full SHA (#2744)
Changelog

Sourced from pypa/cibuildwheel's changelog.

v3.4.1

2 April 2026

  • ⚠️ Building for the experimental CPython 3.13 free-threading variant is now deprecated. That functionality will be removed in the next minor release. The enable option cpython-freethreading is therefore also deprecated. Builds specifying enable = "all" no longer select cpython-freethreading. CPython 3.14 free-threading support remains available without the enable flag. (#2787)
  • 🐛 iOS builds will no longer skip repair-wheel-command if it's defined in config (#2761)
  • 🐛 Fix bug causing uv to fail when environments define PYTHON_VERSION or UV_PYTHON, conflicting with our venvs (#2795)
  • ✨ cibuildwheel prints the selected build identifiers at the start of the build. (#2785)
  • 🔐 The GitHub Action now references other actions with a full SHA (#2744)
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pypa/cibuildwheel&package-manager=github_actions&previous-version=3.4.0&new-version=3.4.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 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> --- .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 0e513530162..7443b5061ae 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -552,7 +552,7 @@ jobs: run: | make cythonize - name: Build wheels - uses: pypa/cibuildwheel@v3.4.0 + uses: pypa/cibuildwheel@v3.4.1 env: CIBW_SKIP: pp* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }} CIBW_ARCHS_MACOS: x86_64 arm64 universal2 From d20f8d64619256177e9c1a3bd184aaccebd160e1 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:03:54 +0100 Subject: [PATCH 003/191] [PR #12307/f4dc0dee backport][3.14] Fix spurious "Future exception was never retrieved" warning on disconnect during backpressure (#12326) **This is a backport of PR #12307 as merged into master (f4dc0deedbae67a94b0ab86b6a0e748249f26d83).** Co-authored-by: availov <51930102+availov@users.noreply.github.com> --- CHANGES/12281.bugfix.rst | 1 + CONTRIBUTORS.txt | 1 + aiohttp/base_protocol.py | 2 +- tests/test_web_server.py | 53 ++++++++++++++++++++++++++++++++++++++-- 4 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 CHANGES/12281.bugfix.rst diff --git a/CHANGES/12281.bugfix.rst b/CHANGES/12281.bugfix.rst new file mode 100644 index 00000000000..63521a73b1c --- /dev/null +++ b/CHANGES/12281.bugfix.rst @@ -0,0 +1 @@ +Fixed spurious ``Future exception was never retrieved`` warning on disconnect during back-pressure -- by :user:`availov`. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 27d04363b7f..530b6683d54 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -410,6 +410,7 @@ Yegor Roganov Yifei Kong Young-Ho Cha Yuriy Shatrov +Yury Novikov Yury Pliner Yury Selivanov Yusuke Tsutsumi diff --git a/aiohttp/base_protocol.py b/aiohttp/base_protocol.py index 7f01830f4e9..d7d83425b88 100644 --- a/aiohttp/base_protocol.py +++ b/aiohttp/base_protocol.py @@ -97,4 +97,4 @@ async def _drain_helper(self) -> None: if waiter is None: waiter = self._loop.create_future() self._drain_waiter = waiter - await asyncio.shield(waiter) + await waiter diff --git a/tests/test_web_server.py b/tests/test_web_server.py index 09b7d0bc71b..59c5da744a6 100644 --- a/tests/test_web_server.py +++ b/tests/test_web_server.py @@ -1,14 +1,15 @@ import asyncio +import gc import socket from contextlib import suppress -from typing import NoReturn +from typing import Any, NoReturn from unittest import mock import pytest from aiohttp import client, web from aiohttp.http_exceptions import BadHttpMethod, BadStatusLine -from aiohttp.pytest_plugin import AiohttpClient, AiohttpRawServer +from aiohttp.pytest_plugin import AiohttpClient, AiohttpRawServer, AiohttpServer async def test_simple_server(aiohttp_raw_server, aiohttp_client) -> None: @@ -417,3 +418,51 @@ async def on_request(_: web.Request) -> web.Response: assert done_event.is_set() finally: await asyncio.gather(runner.shutdown(), site.stop()) + + +async def test_no_future_warning_on_disconnect_during_backpressure( + aiohttp_server: AiohttpServer, +) -> None: + loop = asyncio.get_running_loop() + exc_handler_calls: list[dict[str, Any]] = [] + original_handler = loop.get_exception_handler() + loop.set_exception_handler(lambda _loop, ctx: exc_handler_calls.append(ctx)) + protocol = None + + async def handler(request: web.Request) -> NoReturn: + nonlocal protocol + protocol = request.protocol + resp = web.StreamResponse() + await resp.prepare(request) + while True: + await resp.write(b"x" * 65536) + + app = web.Application() + app.router.add_route("GET", "/", handler) + # aiohttp_server enables handler_cancellation by default so the handler + # task is cancelled when connection_lost() fires. + server = await aiohttp_server(app) + + # Open a raw asyncio connection so we control exactly when the client + # side closes. + reader, writer = await asyncio.open_connection(server.host, server.port) + writer.write(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n") + await writer.drain() + + try: + # Poll until the server protocol reports that writing is paused. + async def wait_for_backpressure() -> None: + while protocol is None or not protocol.writing_paused: + await asyncio.sleep(0.01) + + await asyncio.wait_for(wait_for_backpressure(), timeout=5.0) + + writer.close() + await asyncio.sleep(0.1) + + gc.collect() + await asyncio.sleep(0) + finally: + loop.set_exception_handler(original_handler) + + assert not exc_handler_calls From 2acea2d7b383f40780d9d8188b260dcea124020e Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 00:56:47 +0100 Subject: [PATCH 004/191] [PR #12328/522439ac backport][3.14] Fix GunicornWebWorker failing to reset SIGCHLD handler (#12329) **This is a backport of PR #12328 as merged into master (522439ace1c695c5b73d4c27f2d67bba37277fa7).** Co-authored-by: bahtyar <34988899+Bahtya@users.noreply.github.com> --- aiohttp/worker.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/aiohttp/worker.py b/aiohttp/worker.py index 27e8a8a002c..a86573b1d45 100644 --- a/aiohttp/worker.py +++ b/aiohttp/worker.py @@ -191,8 +191,12 @@ def init_signals(self) -> None: # by interrupting system calls signal.siginterrupt(signal.SIGTERM, False) signal.siginterrupt(signal.SIGUSR1, False) - # Reset signals so Gunicorn doesn't swallow subprocess return codes - # See: https://github.com/aio-libs/aiohttp/issues/6130 + + # Reset SIGCHLD to default so Gunicorn doesn't swallow subprocess + # return codes. Without this, workers inherit the master arbiter's + # SIGCHLD handler, causing spurious "Worker exited" errors when + # application code spawns subprocesses. + signal.signal(signal.SIGCHLD, signal.SIG_DFL) def handle_quit(self, sig: int, frame: FrameType | None) -> None: self.alive = False From c68f7712713edcb0f38e8845c33c7c68bf938ca6 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:27:51 +0100 Subject: [PATCH 005/191] [PR #12330/fc67cfdf backport][3.14] Increase some size in client benchmark tests (#12333) **This is a backport of PR #12330 as merged into master (fc67cfdfd7d4bbf53ef76515fae69726626fe256).** Co-authored-by: Sam Bull --- tests/test_benchmarks_client.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/tests/test_benchmarks_client.py b/tests/test_benchmarks_client.py index 5e205549e9c..8e75ef9c040 100644 --- a/tests/test_benchmarks_client.py +++ b/tests/test_benchmarks_client.py @@ -177,14 +177,14 @@ def _run() -> None: loop.run_until_complete(run_client_benchmark()) -def test_one_hundred_get_requests_with_512kib_chunked_payload( +def test_one_hundred_get_requests_with_10mb_chunked_payload( loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient, benchmark: BenchmarkFixture, ) -> None: - """Benchmark 100 GET requests with a payload of 512KiB using read.""" + """Benchmark 100 GET requests with a payload of 10 MiB using read.""" message_count = 100 - payload = b"a" * (2**19) + payload = b"a" * (10 * 2**20) async def handler(request: web.Request) -> web.Response: resp = web.Response(body=payload) @@ -206,14 +206,14 @@ def _run() -> None: loop.run_until_complete(run_client_benchmark()) -def test_one_hundred_get_requests_iter_chunks_on_512kib_chunked_payload( +def test_one_hundred_get_requests_iter_chunks_on_10mb_chunked_payload( loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient, benchmark: BenchmarkFixture, ) -> None: - """Benchmark 100 GET requests with a payload of 512KiB using iter_chunks.""" + """Benchmark 100 GET requests with a payload of 10 MiB using iter_chunks.""" message_count = 100 - payload = b"a" * (2**19) + payload = b"a" * (10 * 2**20) async def handler(request: web.Request) -> web.Response: resp = web.Response(body=payload) @@ -327,14 +327,14 @@ def _run() -> None: loop.run_until_complete(run_client_benchmark()) -def test_one_hundred_get_requests_with_512kib_content_length_payload( +def test_one_hundred_get_requests_with_10mb_content_length_payload( loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient, benchmark: BenchmarkFixture, ) -> None: - """Benchmark 100 GET requests with a payload of 512KiB.""" + """Benchmark 100 GET requests with a payload of 10 MiB.""" message_count = 100 - payload = b"a" * (2**19) + payload = b"a" * (10 * 2**20) headers = {hdrs.CONTENT_LENGTH: str(len(payload))} async def handler(request: web.Request) -> web.Response: @@ -471,14 +471,15 @@ def _run() -> None: loop.run_until_complete(run_client_benchmark()) -def test_ten_streamed_responses_iter_chunked_65536( +def test_ten_streamed_responses_iter_chunked_1mb( loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient, benchmark: BenchmarkFixture, ) -> None: - """Benchmark 10 streamed responses using iter_chunked 65536.""" + """Benchmark 10 streamed responses using iter_chunked 1 MiB.""" message_count = 10 - data = b"x" * 65536 # 64 KiB chunk size, 64 KiB iter_chunked + MB = 2**20 + data = b"x" * 10 * MB async def handler(request: web.Request) -> web.StreamResponse: resp = web.StreamResponse() @@ -494,7 +495,7 @@ async def run_client_benchmark() -> None: client = await aiohttp_client(app) for _ in range(message_count): resp = await client.get("/") - async for _ in resp.content.iter_chunked(65536): + async for _ in resp.content.iter_chunked(MB): pass await client.close() @@ -510,7 +511,7 @@ def test_ten_streamed_responses_iter_chunks( ) -> None: """Benchmark 10 streamed responses using iter_chunks.""" message_count = 10 - data = b"x" * 65536 # 64 KiB chunk size + data = b"x" * 2**20 async def handler(request: web.Request) -> web.StreamResponse: resp = web.StreamResponse() From 4cad2570d7c46868f2da48ec9dac915a700cd25c Mon Sep 17 00:00:00 2001 From: digiscrypt Date: Tue, 7 Apr 2026 18:43:55 +0530 Subject: [PATCH 006/191] [3.14] Harden cookie file permissions in CookieJar.save() (#12335) --- CHANGES/12312.bugfix.rst | 1 + aiohttp/cookiejar.py | 13 +++++++++++-- tests/test_cookiejar.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 CHANGES/12312.bugfix.rst diff --git a/CHANGES/12312.bugfix.rst b/CHANGES/12312.bugfix.rst new file mode 100644 index 00000000000..a7d240ad79c --- /dev/null +++ b/CHANGES/12312.bugfix.rst @@ -0,0 +1 @@ +``Cookiejar.save()`` now uses ``0x600`` permissions to better protect them from being read by other users -- by :user:`digiscrypt`. diff --git a/aiohttp/cookiejar.py b/aiohttp/cookiejar.py index 098e7ee33a5..a0330ec901a 100644 --- a/aiohttp/cookiejar.py +++ b/aiohttp/cookiejar.py @@ -5,7 +5,7 @@ import heapq import itertools import json -import os # noqa +import os import pathlib import pickle import re @@ -174,7 +174,16 @@ def save(self, file_path: PathLike) -> None: if attr_val: morsel_data[attr] = attr_val data[key][name] = morsel_data - with file_path.open(mode="w", encoding="utf-8") as f: + + # Cookie persistence may include authentication/session tokens. + # Use 0o600 at creation time to avoid umask-dependent overexposure + # and enforce least-privilege access to sensitive credential data. + with open( + file_path, + mode="w", + encoding="utf-8", + opener=lambda path, flags: os.open(path, flags, 0o600), + ) as f: json.dump(data, f, indent=2) def load(self, file_path: PathLike) -> None: diff --git a/tests/test_cookiejar.py b/tests/test_cookiejar.py index 00586145524..d3001191fbd 100644 --- a/tests/test_cookiejar.py +++ b/tests/test_cookiejar.py @@ -3,8 +3,10 @@ import heapq import itertools import logging +import os import pathlib import pickle +import stat import sys import unittest from http.cookies import BaseCookie, Morsel, SimpleCookie @@ -1812,6 +1814,40 @@ async def test_save_load_json_secure_cookies(tmp_path: Path) -> None: assert cookie["domain"] == "example.com" +@pytest.mark.skipif( + os.name != "posix", reason="POSIX permission bits are required for this test" +) +def test_save_creates_private_cookie_file(tmp_path: Path, loop) -> None: + file_path = tmp_path / "private-cookies.json" + jar = CookieJar(loop=loop) + jar.update_cookies_from_headers( + ["token=abc123; Path=/"], URL("https://example.com/") + ) + + jar.save(file_path=file_path) + + assert file_path.exists() + assert stat.S_IMODE(file_path.stat().st_mode) == 0o600 + + +@pytest.mark.skipif( + os.name != "posix", reason="POSIX permission bits are required for this test" +) +def test_save_preserves_existing_cookie_file_permissions(tmp_path: Path, loop) -> None: + file_path = tmp_path / "existing-cookies.json" + file_path.write_text("{}", encoding="utf-8") + file_path.chmod(0o644) + + jar = CookieJar(loop=loop) + jar.update_cookies_from_headers( + ["token=abc123; Path=/"], URL("https://example.com/") + ) + + jar.save(file_path=file_path) + + assert stat.S_IMODE(file_path.stat().st_mode) == 0o644 + + async def test_cookie_jar_unsafe_property() -> None: jar_safe = CookieJar() assert jar_safe.unsafe is False From f2c0280fe7d9857378add2619abd4661b1b13e1c Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 00:19:42 +0100 Subject: [PATCH 007/191] [PR #12350/7eb05774 backport][3.14] Fix octal tests (#12351) **This is a backport of PR #12350 as merged into master (7eb057747aa8ac6b0031fd032b8e2bda53d98b53).** Co-authored-by: Sam Bull --- tests/test_cookie_helpers.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_cookie_helpers.py b/tests/test_cookie_helpers.py index 767e1eaa34a..fead869d6f3 100644 --- a/tests/test_cookie_helpers.py +++ b/tests/test_cookie_helpers.py @@ -1095,13 +1095,13 @@ def test_parse_set_cookie_headers_date_formats_with_attributes() -> None: @pytest.mark.parametrize( ("header", "expected_name", "expected_value", "expected_coded"), [ - # Test cookie values with octal escape sequences - (r'name="\012newline\012"', "name", "\nnewline\n", r'"\012newline\012"'), + # Test cookie values with octal escape sequences (printable chars only) + (r'name="\050parens\051"', "name", "(parens)", r'"\050parens\051"'), ( - r'tab="\011separated\011values"', - "tab", - "\tseparated\tvalues", - r'"\011separated\011values"', + r'punct="\053plus\053values"', + "punct", + "+plus+values", + r'"\053plus\053values"', ), ( r'mixed="hello\040world\041"', @@ -1110,10 +1110,10 @@ def test_parse_set_cookie_headers_date_formats_with_attributes() -> None: r'"hello\040world\041"', ), ( - r'complex="\042quoted\042 text with \012 newline"', + r'complex="\042quoted\042 text with \055 hyphen"', "complex", - '"quoted" text with \n newline', - r'"\042quoted\042 text with \012 newline"', + '"quoted" text with - hyphen', + r'"\042quoted\042 text with \055 hyphen"', ), ], ) From 0373f7c7a28cb7f36e9a4996cce42bec3d81811d Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 16:50:05 +0100 Subject: [PATCH 008/191] [PR #12349/b5f1aa53 backport][3.14] Add Cython coverage (#12354) **This is a backport of PR #12349 as merged into master (b5f1aa53fdcd01b621990ec7e4da152a3de72fb5).** Co-authored-by: Sam Bull --- .coveragerc-cython.toml | 17 +++++++++++ .github/workflows/ci-cd.yml | 58 +++++++++++++++++++++++++++++++++++++ CHANGES/12349.contrib.rst | 1 + Makefile | 7 +++-- setup.py | 23 +++++++++++++-- 5 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 .coveragerc-cython.toml create mode 100644 CHANGES/12349.contrib.rst diff --git a/.coveragerc-cython.toml b/.coveragerc-cython.toml new file mode 100644 index 00000000000..586dc937786 --- /dev/null +++ b/.coveragerc-cython.toml @@ -0,0 +1,17 @@ +[run] +branch = true +plugins = [ + 'Cython.Coverage', +] +omit = [ + 'site-packages', +] + +[report] +exclude_also = [ + 'if TYPE_CHECKING', + 'assert False', + ': \.\.\.(\s*#.*)?$', + '^ +\.\.\.$', + 'pytest.fail\(' +] diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 7443b5061ae..423f2ff5001 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -401,6 +401,64 @@ jobs: run: python -Im pytest --no-cov --numprocesses=0 -vvvvv --codspeed + cython-coverage: + permissions: + contents: read # to fetch code (actions/checkout) + + name: Cython coverage + needs: gen_llhttp + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: true + - name: Setup Python + id: python-install + uses: actions/setup-python@v6 + with: + python-version: '3.12' + - name: Update pip, wheel, setuptools, build, twine + run: | + python -m pip install -U pip wheel setuptools build twine + - name: Install dependencies + run: | + python -Im pip install -r requirements/test.in -c requirements/test.txt + - name: Uninstall blocbuster + run: python -m pip uninstall blockbuster -y + - name: Restore llhttp generated files + uses: actions/download-artifact@v8 + with: + name: llhttp + path: vendor/llhttp/build/ + - name: Cythonize with linetrace + run: | + make cythonize CYTHON_EXTRA="-X linetrace=True" + - name: Install self + env: + AIOHTTP_CYTHON_TRACE: 1 + run: python -m pip install -e . + - name: Run tests with Cython tracing + env: + COLOR: yes + PIP_USER: 1 + run: >- + pytest tests/test_client_functional.py tests/test_http_parser.py tests/test_http_writer.py tests/test_web_functional.py tests/test_web_response.py tests/test_websocket_parser.py + --cov-config=.coveragerc-cython.toml + -m 'not dev_mode and not autobahn' + shell: bash + - name: Turn coverage into xml + run: | + python -m coverage xml -o cython-coverage.xml --rcfile=.coveragerc-cython.toml + - name: Upload coverage + uses: codecov/codecov-action@v6 + with: + files: ./cython-coverage.xml + disable_search: true + flags: cython-coverage + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true + check: # This job does nothing and is only used for the branch protection if: always() diff --git a/CHANGES/12349.contrib.rst b/CHANGES/12349.contrib.rst new file mode 100644 index 00000000000..12aeb069354 --- /dev/null +++ b/CHANGES/12349.contrib.rst @@ -0,0 +1 @@ +Added a CI job to measure Cython coverage -- by :user:`Dreamsorcerer`. diff --git a/Makefile b/Makefile index 29dd75cd53c..1d85f8cdb23 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ to-hash-one = $(dir $1).hash/$(addsuffix .hash,$(notdir $1)) to-hash = $(foreach fname,$1,$(call to-hash-one,$(fname))) +CYTHON_EXTRA ?= CYS := $(wildcard aiohttp/*.pyx) $(wildcard aiohttp/*.pyi) $(wildcard aiohttp/*.pxd) $(wildcard aiohttp/_websocket/*.pyx) $(wildcard aiohttp/_websocket/*.pyi) $(wildcard aiohttp/_websocket/*.pxd) PYXS := $(wildcard aiohttp/*.pyx) $(wildcard aiohttp/_websocket/*.pyx) CS := $(wildcard aiohttp/*.c) $(wildcard aiohttp/_websocket/*.c) @@ -59,14 +60,14 @@ aiohttp/_find_header.c: $(call to-hash,aiohttp/hdrs.py ./tools/gen.py) # Special case for reader since we want to be able to disable # the extension with AIOHTTP_NO_EXTENSIONS aiohttp/_websocket/reader_c.c: aiohttp/_websocket/reader_c.py - cython -3 -X freethreading_compatible=True -o $@ $< -I aiohttp -Werror + cython -3 -X freethreading_compatible=True $(CYTHON_EXTRA) -o $@ $< -I aiohttp -Werror # _find_headers generator creates _headers.pyi as well aiohttp/%.c: aiohttp/%.pyx $(call to-hash,$(CYS)) aiohttp/_find_header.c - cython -3 -X freethreading_compatible=True -o $@ $< -I aiohttp -Werror + cython -3 -X freethreading_compatible=True $(CYTHON_EXTRA) -o $@ $< -I aiohttp -Werror aiohttp/_websocket/%.c: aiohttp/_websocket/%.pyx $(call to-hash,$(CYS)) - cython -3 -X freethreading_compatible=True -o $@ $< -I aiohttp -Werror + cython -3 -X freethreading_compatible=True $(CYTHON_EXTRA) -o $@ $< -I aiohttp -Werror vendor/llhttp/node_modules: vendor/llhttp/package.json cd vendor/llhttp; npm ci diff --git a/setup.py b/setup.py index e88566b435e..9f910fa823a 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,7 @@ os.environ.get("AIOHTTP_USE_SYSTEM_DEPS", os.environ.get("USE_SYSTEM_DEPS")) ) NO_EXTENSIONS: bool = bool(os.environ.get("AIOHTTP_NO_EXTENSIONS")) +CYTHON_TRACING: bool = bool(os.environ.get("AIOHTTP_CYTHON_TRACE")) HERE = pathlib.Path(__file__).parent IS_GIT_REPO = (HERE / ".git").exists() @@ -54,8 +55,16 @@ "include_dirs": ["vendor/llhttp/build"], } +cython_trace_macros = [("CYTHON_TRACE", 1)] if CYTHON_TRACING else [] +if cython_trace_macros: + llhttp_kwargs.setdefault("define_macros", []).extend(cython_trace_macros) + extensions = [ - Extension("aiohttp._websocket.mask", ["aiohttp/_websocket/mask.c"]), + Extension( + "aiohttp._websocket.mask", + ["aiohttp/_websocket/mask.c"], + define_macros=cython_trace_macros, + ), Extension( "aiohttp._http_parser", [ @@ -65,8 +74,16 @@ ], **llhttp_kwargs, ), - Extension("aiohttp._http_writer", ["aiohttp/_http_writer.c"]), - Extension("aiohttp._websocket.reader_c", ["aiohttp/_websocket/reader_c.c"]), + Extension( + "aiohttp._http_writer", + ["aiohttp/_http_writer.c"], + define_macros=cython_trace_macros, + ), + Extension( + "aiohttp._websocket.reader_c", + ["aiohttp/_websocket/reader_c.c"], + define_macros=cython_trace_macros, + ), ] From b502ae655c8788b469dcc832923a85d661719699 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sun, 12 Apr 2026 21:35:00 +0100 Subject: [PATCH 009/191] Allow decompression to continue after exceeding max_length (#11966) (#12355) (cherry picked from commit 0c7ce3485dbcda196e125b4ea913a67e6b54f26c) --- .github/workflows/ci-cd.yml | 5 +- CHANGES/11966.feature.rst | 8 + aiohttp/_cparser.pxd | 1 + aiohttp/_http_parser.pyx | 121 ++++++-- aiohttp/_websocket/writer.py | 2 +- aiohttp/base_protocol.py | 33 +- aiohttp/client.py | 4 +- aiohttp/client_proto.py | 19 +- aiohttp/compression_utils.py | 43 ++- aiohttp/http_exceptions.py | 2 +- aiohttp/http_parser.py | 217 +++++++++----- aiohttp/multipart.py | 27 +- aiohttp/payload.py | 2 +- aiohttp/streams.py | 40 ++- aiohttp/web_exceptions.py | 8 +- aiohttp/web_protocol.py | 55 ++-- aiohttp/web_request.py | 16 +- aiohttp/web_ws.py | 2 +- setup.cfg | 2 + tests/test_base_protocol.py | 22 +- tests/test_benchmarks_http_websocket.py | 4 +- tests/test_client_functional.py | 42 +-- tests/test_client_proto.py | 5 +- tests/test_flowcontrol_streams.py | 7 +- tests/test_http_parser.py | 382 ++++++++++++++++++++---- tests/test_http_writer.py | 8 +- tests/test_multipart.py | 70 ++++- tests/test_payload.py | 4 +- tests/test_streams.py | 40 +-- tests/test_web_functional.py | 44 ++- tests/test_web_protocol.py | 44 +-- tests/test_web_request.py | 23 +- tests/test_websocket_parser.py | 5 +- tests/test_websocket_writer.py | 4 +- 34 files changed, 937 insertions(+), 374 deletions(-) create mode 100644 CHANGES/11966.feature.rst diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 423f2ff5001..34488f65681 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -407,7 +407,10 @@ jobs: name: Cython coverage needs: gen_llhttp - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu, windows] + runs-on: ${{ matrix.os }}-latest steps: - name: Checkout uses: actions/checkout@v6 diff --git a/CHANGES/11966.feature.rst b/CHANGES/11966.feature.rst new file mode 100644 index 00000000000..9f298f98e12 --- /dev/null +++ b/CHANGES/11966.feature.rst @@ -0,0 +1,8 @@ +Large overhaul of parser/decompression code. + +The zip bomb security fix in 3.13 stopped highly compressed payloads +from being decompressed, regardless of validity. Now aiohttp will +decompress such payloads in chunks of 256+ KiB, allowing safe decompression +of such payloads. + +-- by :user:`Dreamsorcerer`. diff --git a/aiohttp/_cparser.pxd b/aiohttp/_cparser.pxd index 1b3be6d4efb..cc7ef58d664 100644 --- a/aiohttp/_cparser.pxd +++ b/aiohttp/_cparser.pxd @@ -145,6 +145,7 @@ cdef extern from "llhttp.h": int llhttp_should_keep_alive(const llhttp_t* parser) + void llhttp_resume(llhttp_t* parser) void llhttp_resume_after_upgrade(llhttp_t* parser) llhttp_errno_t llhttp_get_errno(const llhttp_t* parser) diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index 5da835bc642..e5e3bec8689 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -46,7 +46,8 @@ include "_headers.pxi" from aiohttp cimport _find_header -ALLOWED_UPGRADES = frozenset({"websocket"}) + +cdef frozenset ALLOWED_UPGRADES = frozenset({"websocket"}) DEF DEFAULT_FREELIST_SIZE = 250 cdef extern from "Python.h": @@ -69,7 +70,7 @@ cdef object CONTENT_ENCODING = hdrs.CONTENT_ENCODING cdef object EMPTY_PAYLOAD = _EMPTY_PAYLOAD cdef object StreamReader = _StreamReader cdef object DeflateBuffer = _DeflateBuffer -cdef bytes EMPTY_BYTES = b"" +cdef tuple EMPTY_FEED_DATA_RESULT = ((), False, b"") # RFC 9110 singleton headers — duplicates are rejected in strict mode. # In lax mode (response parser default), the check is skipped entirely @@ -298,7 +299,7 @@ cdef class HttpParser: bint _has_value int _header_name_size - object _protocol + readonly object protocol object _loop object _timer @@ -309,6 +310,7 @@ cdef class HttpParser: bint _read_until_eof bint _lax + bytes _tail bint _started object _url bytearray _buf @@ -319,6 +321,9 @@ cdef class HttpParser: list _raw_headers bint _upgraded list _messages + bint _more_data_available + bint _paused + bint _eof_pending object _payload bint _payload_error object _payload_exception @@ -359,18 +364,22 @@ cdef class HttpParser: self._cparser.data = self self._cparser.content_length = 0 - self._protocol = protocol + self.protocol = protocol self._loop = loop self._timer = timer self._buf = bytearray() + self._more_data_available = False + self._paused = False + self._eof_pending = False self._payload = None self._payload_error = 0 self._payload_exception = payload_exception self._messages = [] - self._raw_name = EMPTY_BYTES - self._raw_value = EMPTY_BYTES + self._raw_name = b"" + self._raw_value = b"" + self._tail = b"" self._has_value = False self._header_name_size = 0 @@ -401,7 +410,7 @@ cdef class HttpParser: cdef _process_header(self): cdef str value - if self._raw_name is not EMPTY_BYTES: + if self._raw_name != b"": name = find_header(self._raw_name) value = self._raw_value.decode('utf-8', 'surrogateescape') @@ -426,20 +435,20 @@ cdef class HttpParser: self._has_value = False self._header_name_size = 0 self._raw_headers.append((self._raw_name, self._raw_value)) - self._raw_name = EMPTY_BYTES - self._raw_value = EMPTY_BYTES + self._raw_name = b"" + self._raw_value = b"" cdef _on_header_field(self, char* at, size_t length): if self._has_value: self._process_header() - if self._raw_name is EMPTY_BYTES: + if self._raw_name == b"": self._raw_name = at[:length] else: self._raw_name += at[:length] cdef _on_header_value(self, char* at, size_t length): - if self._raw_value is EMPTY_BYTES: + if self._raw_value == b"": self._raw_value = at[:length] else: self._raw_value += at[:length] @@ -495,14 +504,14 @@ cdef class HttpParser: self._read_until_eof) ): payload = StreamReader( - self._protocol, timer=self._timer, loop=self._loop, + self.protocol, timer=self._timer, loop=self._loop, limit=self._limit) else: payload = EMPTY_PAYLOAD self._payload = payload if encoding is not None and self._auto_decompress: - self._payload = DeflateBuffer(payload, encoding) + self._payload = DeflateBuffer(payload, encoding, max_decompress_size=self._limit) if not self._response_with_body: payload = EMPTY_PAYLOAD @@ -535,6 +544,10 @@ cdef class HttpParser: ### Public API ### + def pause_reading(self): + assert self._payload is not None + self._paused = True + def feed_eof(self): cdef bytes desc @@ -549,18 +562,52 @@ cdef class HttpParser: desc = cparser.llhttp_get_error_reason(self._cparser) raise PayloadEncodingError(desc.decode('latin-1')) else: + self._eof_pending = True + while self._more_data_available: + if self._paused: + self._paused = False + return # Will resume via feed_data(b"") later + self._more_data_available = self._payload.feed_data(b"", 0) self._payload.feed_eof() + self._payload = None + self._more_data_available = False + self._eof_pending = False elif self._started: self._on_headers_complete() if self._messages: return self._messages[-1][0] - def feed_data(self, data): + def feed_data(self, incoming_data): cdef: size_t data_len size_t nb char* base cdef cparser.llhttp_errno_t errno + cdef bytes data + + # Proactor loop sends bytearray. + # Ensure cython sees `data` as bytes + if type(incoming_data) is not bytes: + data = bytes(incoming_data) + else: + data = incoming_data + + if self._tail: + data, self._tail = self._tail + data, b"" + + if self._more_data_available: + result = cb_on_body(self._cparser, b"", 0) + if result is cparser.HPE_PAUSED: + self._tail = data + return EMPTY_FEED_DATA_RESULT + + if self._eof_pending: + self._payload.feed_eof() + self._payload = None + self._eof_pending = False + # We can't have new messages here, otherwise we wouldn't have + # received EOF. + return EMPTY_FEED_DATA_RESULT PyObject_GetBuffer(data, &self.py_buf, PyBUF_SIMPLE) # Cache buffer pointer before PyBuffer_Release to avoid use-after-release. @@ -574,12 +621,15 @@ cdef class HttpParser: if errno is cparser.HPE_PAUSED_UPGRADE: cparser.llhttp_resume_after_upgrade(self._cparser) - nb = cparser.llhttp_get_error_pos(self._cparser) - base + elif errno is cparser.HPE_PAUSED: + cparser.llhttp_resume(self._cparser) + pos = cparser.llhttp_get_error_pos(self._cparser) - base + self._tail = data[pos:] PyBuffer_Release(&self.py_buf) - if errno not in (cparser.HPE_OK, cparser.HPE_PAUSED_UPGRADE): + if errno not in (cparser.HPE_OK, cparser.HPE_PAUSED, cparser.HPE_PAUSED_UPGRADE): if self._payload_error == 0: if self._last_error is not None: ex = self._last_error @@ -603,8 +653,9 @@ cdef class HttpParser: if self._upgraded: return messages, True, data[nb:] - else: - return messages, False, b"" + if not messages: # Shortcut to reduce Python overhead + return EMPTY_FEED_DATA_RESULT + return messages, False, b"" def set_upgraded(self, val): self._upgraded = val @@ -799,19 +850,27 @@ cdef int cb_on_body(cparser.llhttp_t* parser, const char *at, size_t length) except -1: cdef HttpParser pyparser = parser.data cdef bytes body = at[:length] - try: - pyparser._payload.feed_data(body, length) - except BaseException as underlying_exc: - reraised_exc = underlying_exc - if pyparser._payload_exception is not None: - reraised_exc = pyparser._payload_exception(str(underlying_exc)) - - set_exception(pyparser._payload, reraised_exc, underlying_exc) - - pyparser._payload_error = 1 - return -1 - else: - return 0 + while body or pyparser._more_data_available: + try: + pyparser._more_data_available = pyparser._payload.feed_data(body, length) + except BaseException as underlying_exc: + reraised_exc = underlying_exc + if pyparser._payload_exception is not None: + reraised_exc = pyparser._payload_exception(str(underlying_exc)) + + set_exception(pyparser._payload, reraised_exc, underlying_exc) + + pyparser._payload_error = 1 + pyparser._paused = False + return -1 + body = b"" + length = 0 + + if pyparser._paused: + pyparser._paused = False + return cparser.HPE_PAUSED + pyparser._paused = False + return 0 cdef int cb_on_message_complete(cparser.llhttp_t* parser) except -1: diff --git a/aiohttp/_websocket/writer.py b/aiohttp/_websocket/writer.py index 0d5f56f4b81..5e4bbf62f90 100644 --- a/aiohttp/_websocket/writer.py +++ b/aiohttp/_websocket/writer.py @@ -21,7 +21,7 @@ ) from .models import WS_DEFLATE_TRAILING, WSMsgType -DEFAULT_LIMIT: Final[int] = 2**16 +DEFAULT_LIMIT: Final[int] = 2**18 # WebSocket opcode boundary: opcodes 0-7 are data frames, 8-15 are control frames # Control frames (ping, pong, close) are never compressed diff --git a/aiohttp/base_protocol.py b/aiohttp/base_protocol.py index d7d83425b88..f1f6edc3836 100644 --- a/aiohttp/base_protocol.py +++ b/aiohttp/base_protocol.py @@ -1,26 +1,35 @@ import asyncio -from typing import cast +from typing import TYPE_CHECKING, Any, cast from .client_exceptions import ClientConnectionResetError from .helpers import set_exception from .tcp_helpers import tcp_nodelay +if TYPE_CHECKING: + from .http_parser import HttpParser + class BaseProtocol(asyncio.Protocol): __slots__ = ( "_loop", "_paused", + "_parser", "_drain_waiter", "_connection_lost", "_reading_paused", + "_upgraded", "transport", ) - def __init__(self, loop: asyncio.AbstractEventLoop) -> None: + def __init__( + self, loop: asyncio.AbstractEventLoop, parser: "HttpParser[Any] | None" = None + ) -> None: self._loop: asyncio.AbstractEventLoop = loop self._paused = False self._drain_waiter: asyncio.Future[None] | None = None self._reading_paused = False + self._parser = parser + self._upgraded = False self.transport: asyncio.Transport | None = None @@ -48,15 +57,27 @@ def resume_writing(self) -> None: waiter.set_result(None) def pause_reading(self) -> None: - if not self._reading_paused and self.transport is not None: + self._reading_paused = True + # Parser shouldn't be paused on websockets. + if not self._upgraded: + assert self._parser is not None + self._parser.pause_reading() + if self.transport is not None: try: self.transport.pause_reading() except (AttributeError, NotImplementedError, RuntimeError): pass - self._reading_paused = True - def resume_reading(self) -> None: - if self._reading_paused and self.transport is not None: + def resume_reading(self, resume_parser: bool = True) -> None: + self._reading_paused = False + + # This will resume parsing any unprocessed data from the last pause. + if not self._upgraded and resume_parser: + self.data_received(b"") + + # Reading may have been paused again in the above call if there was a lot of + # compressed data still pending. + if not self._reading_paused and self.transport is not None: try: self.transport.resume_reading() except (AttributeError, NotImplementedError, RuntimeError): diff --git a/aiohttp/client.py b/aiohttp/client.py index d797aa31021..890555e5783 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -333,7 +333,7 @@ def __init__( trust_env: bool = False, requote_redirect_url: bool = True, trace_configs: list[TraceConfig] | None = None, - read_bufsize: int = 2**16, + read_bufsize: int = 2**18, max_line_size: int = 8190, max_field_size: int = 8190, max_headers: int = 128, @@ -1294,7 +1294,7 @@ async def _ws_connect( transport = conn.transport assert transport is not None - reader = WebSocketDataQueue(conn_proto, 2**16, loop=self._loop) + reader = WebSocketDataQueue(conn_proto, 2**18, loop=self._loop) writer = WebSocketWriter( conn_proto, transport, diff --git a/aiohttp/client_proto.py b/aiohttp/client_proto.py index dedde857704..011fc7217fd 100644 --- a/aiohttp/client_proto.py +++ b/aiohttp/client_proto.py @@ -26,7 +26,7 @@ class ResponseHandler(BaseProtocol, DataQueue[tuple[RawResponseMessage, StreamRe """Helper class to adapt between Protocol and StreamReader.""" def __init__(self, loop: asyncio.AbstractEventLoop) -> None: - BaseProtocol.__init__(self, loop=loop) + BaseProtocol.__init__(self, loop=loop, parser=None) DataQueue.__init__(self, loop) self._should_close = False @@ -37,10 +37,7 @@ def __init__(self, loop: asyncio.AbstractEventLoop) -> None: self._data_received_cb: Callable[[], None] | None = None self._timer = None - self._tail = b"" - self._upgraded = False - self._parser: HttpResponseParser | None = None self._read_timeout: float | None = None self._read_timeout_handle: asyncio.TimerHandle | None = None @@ -191,8 +188,8 @@ def pause_reading(self) -> None: super().pause_reading() self._drop_timeout() - def resume_reading(self) -> None: - super().resume_reading() + def resume_reading(self, resume_parser: bool = True) -> None: + super().resume_reading(resume_parser) self._reschedule_timeout() def set_exception( @@ -233,7 +230,7 @@ def set_response_params( read_until_eof: bool = False, auto_decompress: bool = True, read_timeout: float | None = None, - read_bufsize: int = 2**16, + read_bufsize: int = 2**18, timeout_ceil_threshold: float = 5, max_line_size: int = 8190, max_field_size: int = 8190, @@ -298,10 +295,10 @@ def _on_read_timeout(self) -> None: set_exception(self._payload, exc) def data_received(self, data: bytes) -> None: - self._reschedule_timeout() - - if not data: - return + # If no data, then we are resuming decompression. We haven't received + # data from the socket, so we can avoid the reschedule overhead. + if data: + self._reschedule_timeout() # custom payload parser - currently always WebSocketReader if self._payload_parser is not None: diff --git a/aiohttp/compression_utils.py b/aiohttp/compression_utils.py index 562b2813401..92c44da53c8 100644 --- a/aiohttp/compression_utils.py +++ b/aiohttp/compression_utils.py @@ -34,7 +34,9 @@ MAX_SYNC_CHUNK_SIZE = 4096 -DEFAULT_MAX_DECOMPRESS_SIZE = 2**25 # 32MiB +# Matches the max size we receive from sockets: +# https://github.com/python/cpython/blob/1857a40807daeae3a1bf5efb682de9c9ae6df845/Lib/asyncio/selector_events.py#L766 +DEFAULT_MAX_DECOMPRESS_SIZE = 256 * 1024 # Unlimited decompression constants - different libraries use different conventions ZLIB_MAX_LENGTH_UNLIMITED = 0 # zlib uses 0 to mean unlimited @@ -53,6 +55,9 @@ def flush(self, length: int = ..., /) -> bytes: ... @property def eof(self) -> bool: ... + @property + def unconsumed_tail(self) -> bytes: ... + class ZLibBackendProtocol(Protocol): MAX_WBITS: int @@ -179,6 +184,11 @@ async def decompress( ) return self.decompress_sync(data, max_length) + @property + @abstractmethod + def data_available(self) -> bool: + """Return True if more output is available by passing b"".""" + class ZLibCompressor: def __init__( @@ -267,11 +277,17 @@ def __init__( self._mode = encoding_to_mode(encoding, suppress_deflate_header) self._zlib_backend: Final = ZLibBackendWrapper(ZLibBackend._zlib_backend) self._decompressor = self._zlib_backend.decompressobj(wbits=self._mode) + self._last_empty = False def decompress_sync( self, data: Buffer, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED ) -> bytes: - return self._decompressor.decompress(data, max_length) + result = self._decompressor.decompress( + self._decompressor.unconsumed_tail + data, max_length + ) + # Only way to know that isal has no further data is checking we get no output + self._last_empty = result == b"" + return result def flush(self, length: int = 0) -> bytes: return ( @@ -280,6 +296,10 @@ def flush(self, length: int = 0) -> bytes: else self._decompressor.flush() ) + @property + def data_available(self) -> bool: + return bool(self._decompressor.unconsumed_tail) or not self._last_empty + @property def eof(self) -> bool: return self._decompressor.eof @@ -301,6 +321,7 @@ def __init__( "Please install `Brotli` module" ) self._obj = brotli.Decompressor() + self._last_empty = False super().__init__(executor=executor, max_sync_chunk_size=max_sync_chunk_size) def decompress_sync( @@ -308,8 +329,12 @@ def decompress_sync( ) -> bytes: """Decompress the given data.""" if hasattr(self._obj, "decompress"): - return cast(bytes, self._obj.decompress(data, max_length)) - return cast(bytes, self._obj.process(data, max_length)) + result = cast(bytes, self._obj.decompress(data, max_length)) + else: + result = cast(bytes, self._obj.process(data, max_length)) + # Only way to know that brotli has no further data is checking we get no output + self._last_empty = result == b"" + return result def flush(self) -> bytes: """Flush the decompressor.""" @@ -317,6 +342,10 @@ def flush(self) -> bytes: return cast(bytes, self._obj.flush()) return b"" + @property + def data_available(self) -> bool: + return not self._obj.is_finished() and not self._last_empty + class ZSTDDecompressor(DecompressionBaseHandler): def __init__( @@ -373,3 +402,9 @@ def decompress_sync( def flush(self) -> bytes: return b"" + + @property + def data_available(self) -> bool: + return ( + not self._obj.needs_input and not self._obj.eof + ) or self._pending_unused_data is not None diff --git a/aiohttp/http_exceptions.py b/aiohttp/http_exceptions.py index daa8da81a3c..f21a97f0170 100644 --- a/aiohttp/http_exceptions.py +++ b/aiohttp/http_exceptions.py @@ -74,7 +74,7 @@ class ContentLengthError(PayloadEncodingError): class DecompressSizeError(PayloadEncodingError): - """Decompressed size exceeds the configured limit.""" + """Deprecated. Removed in v4.""" class LineTooLong(BadHttpMessage): diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py index 161b474ac38..3fb5368e2bf 100644 --- a/aiohttp/http_parser.py +++ b/aiohttp/http_parser.py @@ -5,7 +5,16 @@ from contextlib import suppress from enum import IntEnum from re import Pattern -from typing import Any, ClassVar, Final, Generic, Literal, NamedTuple, TypeVar +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + Final, + Generic, + Literal, + NamedTuple, + TypeVar, +) from multidict import CIMultiDict, CIMultiDictProxy, istr from yarl import URL @@ -35,7 +44,6 @@ BadStatusLine, ContentEncodingError, ContentLengthError, - DecompressSizeError, InvalidHeader, InvalidURLError, LineTooLong, @@ -45,6 +53,9 @@ from .streams import EMPTY_PAYLOAD, StreamReader from .typedefs import RawHeaders +if TYPE_CHECKING: + from .client_proto import ResponseHandler + __all__ = ( "HeadersParser", "HttpParser", @@ -124,6 +135,12 @@ class RawResponseMessage(NamedTuple): _MsgT = TypeVar("_MsgT", RawRequestMessage, RawResponseMessage) +class PayloadState(IntEnum): + PAYLOAD_COMPLETE = 0 + PAYLOAD_NEEDS_INPUT = 1 + PAYLOAD_HAS_PENDING_INPUT = 2 + + class ParseState(IntEnum): PARSE_NONE = 0 @@ -276,6 +293,7 @@ def __init__( self._upgraded = False self._payload = None self._payload_parser: HttpPayloadParser | None = None + self._payload_has_more_data = False self._auto_decompress = auto_decompress self._limit = limit self._headers_parser = HeadersParser( @@ -288,10 +306,15 @@ def parse_message(self, lines: list[bytes]) -> _MsgT: ... @abc.abstractmethod def _is_chunked_te(self, te: str) -> bool: ... + def pause_reading(self) -> None: + assert self._payload_parser is not None + self._payload_parser.pause_reading() + def feed_eof(self) -> _MsgT | None: if self._payload_parser is not None: self._payload_parser.feed_eof() - self._payload_parser = None + if self._payload_parser.done: + self._payload_parser = None else: # try to extract partial message if self._tail: @@ -325,8 +348,7 @@ def feed_data( max_line_length = self.max_line_size should_close = False - while start_pos < data_len: - + while start_pos < data_len or self._payload_has_more_data: # read HTTP message (request/response line + headers), \r\n\r\n # and split by lines if self._payload_parser is None and not self._upgraded: @@ -420,6 +442,7 @@ def get_content_length() -> int | None: max_line_size=self.max_line_size, max_field_size=self.max_field_size, max_trailers=max_trailers, + limit=self._limit, ) if not payload_parser.done: self._payload_parser = payload_parser @@ -442,6 +465,7 @@ def get_content_length() -> int | None: max_line_size=self.max_line_size, max_field_size=self.max_field_size, max_trailers=max_trailers, + limit=self._limit, ) elif not empty_body and length is None and self.read_until_eof: payload = StreamReader( @@ -464,6 +488,7 @@ def get_content_length() -> int | None: max_line_size=self.max_line_size, max_field_size=self.max_field_size, max_trailers=max_trailers, + limit=self._limit, ) if not payload_parser.done: self._payload_parser = payload_parser @@ -485,11 +510,13 @@ def get_content_length() -> int | None: break # feed payload - elif data and start_pos < data_len: + else: assert not self._lines assert self._payload_parser is not None try: - eof, data = self._payload_parser.feed_data(data[start_pos:], SEP) + payload_state, data = self._payload_parser.feed_data( + data[start_pos:], SEP + ) except Exception as underlying_exc: reraised_exc: BaseException = underlying_exc if self.payload_exception is not None: @@ -501,20 +528,25 @@ def get_content_length() -> int | None: underlying_exc, ) - eof = True + payload_state = PayloadState.PAYLOAD_COMPLETE data = b"" if isinstance( underlying_exc, (InvalidHeader, TransferEncodingError) ): raise - if eof: - start_pos = 0 - data_len = len(data) - self._payload_parser = None - continue - else: - break + self._payload_has_more_data = ( + payload_state == PayloadState.PAYLOAD_HAS_PENDING_INPUT + ) + + if payload_state is not PayloadState.PAYLOAD_COMPLETE: + # We've either consumed all available data, or we're pausing + # until the reader buffer is freed up. + break + + start_pos = 0 + data_len = len(data) + self._payload_parser = None if data and start_pos < data_len: data = data[start_pos:] @@ -689,6 +721,8 @@ class HttpResponseParser(HttpParser[RawResponseMessage]): Returns RawResponseMessage. """ + protocol: "ResponseHandler" + # Lax mode should only be enabled on response parser. lax = not DEBUG @@ -783,8 +817,10 @@ def __init__( max_line_size: int = 8190, max_field_size: int = 8190, max_trailers: int = 128, + limit: int = DEFAULT_MAX_DECOMPRESS_SIZE, ) -> None: self._length = 0 + self._paused = False self._type = ParseState.PARSE_UNTIL_EOF self._chunk = ChunkState.PARSE_CHUNKED_SIZE self._chunk_size = 0 @@ -795,13 +831,15 @@ def __init__( self._max_line_size = max_line_size self._max_field_size = max_field_size self._max_trailers = max_trailers + self._more_data_available = False self._trailer_lines: list[bytes] = [] self.done = False + self._eof_pending = False # payload decompression wrapper if response_with_body and compression and self._auto_decompress: real_payload: StreamReader | DeflateBuffer = DeflateBuffer( - payload, compression + payload, compression, max_decompress_size=limit ) else: real_payload = payload @@ -823,9 +861,20 @@ def __init__( self.payload = real_payload + def pause_reading(self) -> None: + self._paused = True + def feed_eof(self) -> None: if self._type == ParseState.PARSE_UNTIL_EOF: + self._eof_pending = True + while self._more_data_available: + if self._paused: + self._paused = False + return # Will resume via feed_data(b"") later + self._more_data_available = self.payload.feed_data(b"", 0) self.payload.feed_eof() + self.done = True + self._eof_pending = False elif self._type == ParseState.PARSE_LENGTH: raise ContentLengthError( "Not enough data to satisfy content length header." @@ -837,41 +886,54 @@ def feed_eof(self) -> None: def feed_data( self, chunk: bytes, SEP: _SEP = b"\r\n", CHUNK_EXT: bytes = b";" - ) -> tuple[bool, bytes]: + ) -> tuple[PayloadState, bytes]: + """Receive a chunk of data to process. + + Return: + PayloadState - The current state of payload processing. + This function may be called with empty bytes after returning + PAYLOAD_HAS_PENDING_INPUT to continue processing after a pause. + bytes - If payload is complete, this is the unconsumed bytes intended for the + next message/payload, b"" otherwise. + """ # Read specified amount of bytes if self._type == ParseState.PARSE_LENGTH: + if self._chunk_tail: + chunk = self._chunk_tail + chunk + self._chunk_tail = b"" + required = self._length - chunk_len = len(chunk) - - if required >= chunk_len: - self._length = required - chunk_len - self.payload.feed_data(chunk, chunk_len) - if self._length == 0: - self.payload.feed_eof() - return True, b"" - else: - self._length = 0 - self.payload.feed_data(chunk[:required], required) - self.payload.feed_eof() - return True, chunk[required:] + self._length = max(required - len(chunk), 0) + self._more_data_available = self.payload.feed_data( + chunk[:required], required + ) + while self._more_data_available: + if self._paused: + self._paused = False + self._chunk_tail = chunk[required:] + return PayloadState.PAYLOAD_HAS_PENDING_INPUT, b"" + self._more_data_available = self.payload.feed_data(b"", 0) + if self._length == 0: + self.payload.feed_eof() + return PayloadState.PAYLOAD_COMPLETE, chunk[required:] # Chunked transfer encoding parser elif self._type == ParseState.PARSE_CHUNKED: if self._chunk_tail: - # We should never have a tail if we're inside the payload body. - assert self._chunk != ChunkState.PARSE_CHUNKED_CHUNK - # We should check the length is sane. - max_line_length = self._max_line_size - if self._chunk == ChunkState.PARSE_TRAILERS: - max_line_length = self._max_field_size - if len(self._chunk_tail) > max_line_length: - raise LineTooLong(self._chunk_tail[:100] + b"...", max_line_length) + # We should check the length is sane when not processing payload body. + if self._chunk != ChunkState.PARSE_CHUNKED_CHUNK: + max_line_length = self._max_line_size + if self._chunk == ChunkState.PARSE_TRAILERS: + max_line_length = self._max_field_size + if len(self._chunk_tail) > max_line_length: + raise LineTooLong( + self._chunk_tail[:100] + b"...", max_line_length + ) chunk = self._chunk_tail + chunk self._chunk_tail = b"" - while chunk: - + while chunk or self._more_data_available: # read next chunk size if self._chunk == ChunkState.PARSE_CHUNKED_SIZE: pos = chunk.find(SEP) @@ -911,23 +973,30 @@ def feed_data( self.payload.begin_http_chunk_receiving() else: self._chunk_tail = chunk - return False, b"" + return PayloadState.PAYLOAD_NEEDS_INPUT, b"" # read chunk and feed buffer if self._chunk == ChunkState.PARSE_CHUNKED_CHUNK: + if self._paused: + self._paused = False + self._chunk_tail = chunk + return PayloadState.PAYLOAD_HAS_PENDING_INPUT, b"" + required = self._chunk_size - chunk_len = len(chunk) + self._chunk_size = max(required - len(chunk), 0) + self._more_data_available = self.payload.feed_data( + chunk[:required], required + ) + chunk = chunk[required:] - if required > chunk_len: - self._chunk_size = required - chunk_len - self.payload.feed_data(chunk, chunk_len) - return False, b"" - else: - self._chunk_size = 0 - self.payload.feed_data(chunk[:required], required) - chunk = chunk[required:] - self._chunk = ChunkState.PARSE_CHUNKED_CHUNK_EOF - self.payload.end_http_chunk_receiving() + if self._more_data_available: + continue + + if self._chunk_size: + self._paused = False + return PayloadState.PAYLOAD_NEEDS_INPUT, b"" + self._chunk = ChunkState.PARSE_CHUNKED_CHUNK_EOF + self.payload.end_http_chunk_receiving() # toss the CRLF at the end of the chunk if self._chunk == ChunkState.PARSE_CHUNKED_CHUNK_EOF: @@ -944,13 +1013,13 @@ def feed_data( raise exc else: self._chunk_tail = chunk - return False, b"" + return PayloadState.PAYLOAD_NEEDS_INPUT, b"" if self._chunk == ChunkState.PARSE_TRAILERS: pos = chunk.find(SEP) if pos < 0: # No line found self._chunk_tail = chunk - return False, b"" + return PayloadState.PAYLOAD_NEEDS_INPUT, b"" line = chunk[:pos] chunk = chunk[pos + len(SEP) :] @@ -976,13 +1045,24 @@ def feed_data( finally: self._trailer_lines.clear() self.payload.feed_eof() - return True, chunk + return PayloadState.PAYLOAD_COMPLETE, chunk # Read all bytes until eof elif self._type == ParseState.PARSE_UNTIL_EOF: - self.payload.feed_data(chunk, len(chunk)) + self._more_data_available = self.payload.feed_data(chunk, len(chunk)) + while self._more_data_available: + if self._paused: + self._paused = False + return PayloadState.PAYLOAD_HAS_PENDING_INPUT, b"" + self._more_data_available = self.payload.feed_data(b"", 0) + + if self._eof_pending: + self.payload.feed_eof() + self.done = True + self._eof_pending = False + return PayloadState.PAYLOAD_COMPLETE, b"" - return False, b"" + return PayloadState.PAYLOAD_NEEDS_INPUT, b"" class DeflateBuffer: @@ -1029,10 +1109,7 @@ def set_exception( ) -> None: set_exception(self.out, exc, exc_cause) - def feed_data(self, chunk: bytes, size: int) -> None: - if not size: - return - + def feed_data(self, chunk: bytes, size: int) -> bool: self.size += size self.out.total_compressed_bytes = self.size @@ -1051,9 +1128,8 @@ def feed_data(self, chunk: bytes, size: int) -> None: ) try: - # Decompress with limit + 1 so we can detect if output exceeds limit chunk = self.decompressor.decompress_sync( - chunk, max_length=self._max_decompress_size + 1 + chunk, max_length=self._max_decompress_size ) except Exception: raise ContentEncodingError( @@ -1062,21 +1138,18 @@ def feed_data(self, chunk: bytes, size: int) -> None: self._started_decoding = True - # Check if decompression limit was exceeded - if len(chunk) > self._max_decompress_size: - raise DecompressSizeError( - "Decompressed data exceeds the configured limit of %d bytes" - % self._max_decompress_size - ) - if chunk: self.out.feed_data(chunk, len(chunk)) + return self.decompressor.data_available # type: ignore[no-any-return] def feed_eof(self) -> None: chunk = self.decompressor.flush() + # This should never contain data as we defer the call until exhausting + # the decompression. If .flush() is returning data, this may indicate a + # zip bomb vulnerability as it will decompress all remaining data at once. + assert not chunk - if chunk or self.size > 0: - self.out.feed_data(chunk, len(chunk)) + if self.size > 0: if self.encoding == "deflate" and not self.decompressor.eof: raise ContentEncodingError("deflate") diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py index 5a4f5742fcb..cf990ded777 100644 --- a/aiohttp/multipart.py +++ b/aiohttp/multipart.py @@ -268,6 +268,8 @@ def __init__( subtype: str = "mixed", default_charset: str | None = None, max_decompress_size: int = DEFAULT_MAX_DECOMPRESS_SIZE, + client_max_size: int = sys.maxsize, + max_size_error_cls: type[Exception] = ValueError, ) -> None: self.headers = headers self._boundary = boundary @@ -285,6 +287,8 @@ def __init__( self._content_eof = 0 self._cache: dict[str, Any] = {} self._max_decompress_size = max_decompress_size + self._client_max_size = client_max_size + self._max_size_error_cls = max_size_error_cls def __aiter__(self: Self) -> Self: return self @@ -313,10 +317,14 @@ async def read(self, *, decode: bool = False) -> bytes: data = bytearray() while not self._at_eof: data.extend(await self.read_chunk(self.chunk_size)) + if len(data) > self._client_max_size: + raise self._max_size_error_cls(self._client_max_size) if decode: decoded_data = bytearray() async for d in self.decode_iter(data): decoded_data.extend(d) + if len(decoded_data) > self._client_max_size: + raise self._max_size_error_cls(self._client_max_size) return decoded_data return data @@ -558,6 +566,8 @@ async def _decode_content_async(self, data: bytes) -> AsyncIterator[bytes]: suppress_deflate_header=True, ) yield await d.decompress(data, max_length=self._max_decompress_size) + while d.data_available: + yield await d.decompress(b"", max_length=self._max_decompress_size) else: raise RuntimeError(f"unknown content encoding: {encoding}") @@ -651,8 +661,10 @@ def __init__( headers: Mapping[str, str], content: StreamReader, *, + client_max_size: int = sys.maxsize, max_field_size: int = 8190, max_headers: int = 128, + max_size_error_cls: type[Exception] = ValueError, ) -> None: self._mimetype = parse_mimetype(headers[CONTENT_TYPE]) assert self._mimetype.type == "multipart", "multipart/* content type expected" @@ -663,11 +675,13 @@ def __init__( self.headers = headers self._boundary = ("--" + self._get_boundary()).encode() + self._client_max_size = client_max_size self._content = content self._default_charset: str | None = None self._last_part: MultipartReader | BodyPartReader | None = None self._max_field_size = max_field_size self._max_headers = max_headers + self._max_size_error_cls = max_size_error_cls self._at_eof = False self._at_bof = True self._unread: list[bytes] = [] @@ -766,12 +780,21 @@ def _get_part_reader( if mimetype.type == "multipart": if self.multipart_reader_cls is None: - return type(self)(headers, self._content) + return type(self)( + headers, + self._content, + client_max_size=self._client_max_size, + max_field_size=self._max_field_size, + max_headers=self._max_headers, + max_size_error_cls=self._max_size_error_cls, + ) return self.multipart_reader_cls( headers, self._content, + client_max_size=self._client_max_size, max_field_size=self._max_field_size, max_headers=self._max_headers, + max_size_error_cls=self._max_size_error_cls, ) else: return self.part_reader_cls( @@ -780,6 +803,8 @@ def _get_part_reader( self._content, subtype=self._mimetype.subtype, default_charset=self._default_charset, + client_max_size=self._client_max_size, + max_size_error_cls=self._max_size_error_cls, ) def _get_boundary(self) -> str: diff --git a/aiohttp/payload.py b/aiohttp/payload.py index 5ea00039be7..5b4cf94800c 100644 --- a/aiohttp/payload.py +++ b/aiohttp/payload.py @@ -43,7 +43,7 @@ ) TOO_LARGE_BYTES_BODY: Final[int] = 2**20 # 1 MB -READ_SIZE: Final[int] = 2**16 # 64 KB +READ_SIZE: Final[int] = 2**18 # 256 KiB _CLOSE_FUTURES: set[asyncio.Future[None]] = set() diff --git a/aiohttp/streams.py b/aiohttp/streams.py index 33cd2113d84..bf26b59d49e 100644 --- a/aiohttp/streams.py +++ b/aiohttp/streams.py @@ -140,11 +140,11 @@ def __init__( self._high_water = limit * 2 if loop is None: loop = asyncio.get_event_loop() - # Ensure high_water_chunks >= 3 so it's always > low_water_chunks. - self._high_water_chunks = max(3, limit // 4) - # Use max(2, ...) because there's always at least 1 chunk split remaining + # Use max(4, ...) because there's always at least 1 chunk split remaining # (the current position), so we need low_water >= 2 to allow resume. - self._low_water_chunks = max(2, self._high_water_chunks // 2) + # limit // 16 gets us a reasonable value of 16k with default 256KiB limit. + self._high_water_chunks = max(4, limit // 16) + self._low_water_chunks = self._high_water_chunks // 2 self._loop = loop self._size = 0 self._cursor = 0 @@ -167,7 +167,7 @@ def __repr__(self) -> str: info.append("%d bytes" % self._size) if self._eof: info.append("eof") - if self._low_water != 2**16: # default limit + if self._low_water != 2**18: # default limit info.append("low=%d high=%d" % (self._low_water, self._high_water)) if self._waiter: info.append("w=%r" % self._waiter) @@ -221,8 +221,8 @@ def feed_eof(self) -> None: self._eof_waiter = None set_result(waiter, None) - if self._protocol._reading_paused: - self._protocol.resume_reading() + # At EOF the parser is done, there won't be unprocessed data. + self._protocol.resume_reading(resume_parser=False) for cb in self._eof_callbacks: try: @@ -277,11 +277,11 @@ def unread_data(self, data: bytes) -> None: self._eof_counter = 0 # TODO: size is ignored, remove the param later - def feed_data(self, data: bytes, size: int = 0) -> None: + def feed_data(self, data: bytes, size: int = 0) -> bool: assert not self._eof, "feed_data after feed_eof" if not data: - return + return False data_len = len(data) self._size += data_len @@ -293,8 +293,9 @@ def feed_data(self, data: bytes, size: int = 0) -> None: self._waiter = None set_result(waiter, None) - if self._size > self._high_water and not self._protocol._reading_paused: + if self._size > self._high_water: self._protocol.pause_reading() + return False def begin_http_chunk_receiving(self) -> None: if self._http_chunk_splits is None: @@ -331,10 +332,7 @@ def end_http_chunk_receiving(self) -> None: # If we get too many small chunks before self._high_water is reached, then any # .read() call becomes computationally expensive, and could block the event loop # for too long, hence an additional self._high_water_chunks here. - if ( - len(self._http_chunk_splits) > self._high_water_chunks - and not self._protocol._reading_paused - ): + if len(self._http_chunk_splits) > self._high_water_chunks: self._protocol.pause_reading() # wake up readchunk when end of http chunk received @@ -548,13 +546,9 @@ def _read_nowait_chunk(self, n: int) -> bytes: while chunk_splits and chunk_splits[0] < self._cursor: chunk_splits.popleft() - if ( - self._protocol._reading_paused - and self._size < self._low_water - and ( - self._http_chunk_splits is None - or len(self._http_chunk_splits) < self._low_water_chunks - ) + if self._size < self._low_water and ( + self._http_chunk_splits is None + or len(self._http_chunk_splits) < self._low_water_chunks ): self._protocol.resume_reading() return data @@ -614,8 +608,8 @@ def at_eof(self) -> bool: async def wait_eof(self) -> None: return - def feed_data(self, data: bytes, n: int = 0) -> None: - pass + def feed_data(self, data: bytes, n: int = 0) -> bool: + return False async def readline(self, *, max_line_length: int | None = None) -> bytes: return b"" diff --git a/aiohttp/web_exceptions.py b/aiohttp/web_exceptions.py index 5f065a86add..b5b3b30cb7d 100644 --- a/aiohttp/web_exceptions.py +++ b/aiohttp/web_exceptions.py @@ -315,12 +315,8 @@ class HTTPPreconditionFailed(HTTPClientError): class HTTPRequestEntityTooLarge(HTTPClientError): status_code = 413 - def __init__(self, max_size: float, actual_size: float, **kwargs: Any) -> None: - kwargs.setdefault( - "text", - f"Maximum request body size {max_size} exceeded, " - f"actual body size {actual_size}", - ) + def __init__(self, max_size: float, actual_size: float = 0, **kwargs: Any) -> None: + kwargs.setdefault("text", f"Maximum request body size {max_size} exceeded.") super().__init__(**kwargs) diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py index 68fb266c786..be63b824b45 100644 --- a/aiohttp/web_protocol.py +++ b/aiohttp/web_protocol.py @@ -24,6 +24,7 @@ HttpVersion10, RawRequestMessage, StreamWriter, + WebSocketReader, ) from .http_exceptions import BadHttpMethod from .log import access_logger, server_logger @@ -149,11 +150,8 @@ class RequestHandler(BaseProtocol): "_handler_waiter", "_waiter", "_task_handler", - "_upgrade", "_payload_parser", "_data_received_cb", - "_request_parser", - "_reading_paused", "logger", "debug", "access_log", @@ -184,11 +182,21 @@ def __init__( max_headers: int = 128, max_field_size: int = 8190, lingering_time: float = 10.0, - read_bufsize: int = 2**16, + read_bufsize: int = 2**18, auto_decompress: bool = True, timeout_ceil_threshold: float = 5, ): - super().__init__(loop) + parser = HttpRequestParser( + self, + loop, + read_bufsize, + max_line_size=max_line_size, + max_field_size=max_field_size, + max_headers=max_headers, + payload_exception=RequestPayloadError, + auto_decompress=auto_decompress, + ) + super().__init__(loop, parser) # _request_count is the number of requests processed with the same connection. self._request_count = 0 @@ -216,19 +224,7 @@ def __init__( self._waiter: asyncio.Future[None] | None = None self._handler_waiter: asyncio.Future[None] | None = None self._task_handler: asyncio.Task[None] | None = None - - self._upgrade = False self._payload_parser: Any = None - self._request_parser: HttpRequestParser | None = HttpRequestParser( - self, - loop, - read_bufsize, - max_line_size=max_line_size, - max_field_size=max_field_size, - max_headers=max_headers, - payload_exception=RequestPayloadError, - auto_decompress=auto_decompress, - ) self._timeout_ceil_threshold: float = 5 try: @@ -363,7 +359,7 @@ def connection_lost(self, exc: BaseException | None) -> None: self._manager = None self._request_factory = None self._request_handler = None - self._request_parser = None + self._parser = None if self._keepalive_handle is not None: self._keepalive_handle.cancel() @@ -383,9 +379,10 @@ def connection_lost(self, exc: BaseException | None) -> None: self._payload_parser = None def set_parser( - self, parser: Any, data_received_cb: Callable[[], None] | None = None + self, + parser: WebSocketReader, + data_received_cb: Callable[[], None] | None = None, ) -> None: - # Actual type is WebReader assert self._payload_parser is None self._payload_parser = parser @@ -403,10 +400,10 @@ def data_received(self, data: bytes) -> None: return # parse http messages messages: Sequence[_MsgType] - if self._payload_parser is None and not self._upgrade: - assert self._request_parser is not None + if self._payload_parser is None and not self._upgraded: + assert self._parser is not None try: - messages, upgraded, tail = self._request_parser.feed_data(data) + messages, upgraded, tail = self._parser.feed_data(data) except HttpProcessingError as exc: messages = [ (_ErrInfo(status=400, exc=exc, message=exc.message), EMPTY_PAYLOAD) @@ -423,12 +420,12 @@ def data_received(self, data: bytes) -> None: # don't set result twice waiter.set_result(None) - self._upgrade = upgraded + self._upgraded = upgraded if upgraded and tail: self._message_tail = tail # no parser, just store - elif self._payload_parser is None and self._upgrade and data: + elif self._payload_parser is None and self._upgraded and data: self._message_tail += data # feed payload @@ -691,11 +688,11 @@ async def finish_response( prematurely. """ request._finish() - if self._request_parser is not None: - self._request_parser.set_upgraded(False) - self._upgrade = False + if self._parser is not None: + self._parser.set_upgraded(False) + self._upgraded = False if self._message_tail: - self._request_parser.feed_data(self._message_tail) + self._parser.feed_data(self._message_tail) self._message_tail = b"" try: prepare_meth = resp.prepare diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py index 4bdd7aff606..826a88337a0 100644 --- a/aiohttp/web_request.py +++ b/aiohttp/web_request.py @@ -683,10 +683,8 @@ async def read(self) -> bytes: body.extend(chunk) if self._client_max_size: body_size = len(body) - if body_size >= self._client_max_size: - raise HTTPRequestEntityTooLarge( - max_size=self._client_max_size, actual_size=body_size - ) + if body_size > self._client_max_size: + raise HTTPRequestEntityTooLarge(self._client_max_size) if not chunk: break self._read_bytes = bytes(body) @@ -708,8 +706,10 @@ async def multipart(self) -> MultipartReader: return MultipartReader( self._headers, self._payload, + client_max_size=self._client_max_size, max_field_size=self._protocol.max_field_size, max_headers=self._protocol.max_headers, + max_size_error_cls=HTTPRequestEntityTooLarge, ) async def post(self) -> "MultiDictProxy[str | bytes | FileField]": @@ -760,9 +760,7 @@ async def post(self) -> "MultiDictProxy[str | bytes | FileField]": size += len(decoded_chunk) if 0 < max_size < size: await self._loop.run_in_executor(None, tmp.close) - raise HTTPRequestEntityTooLarge( - max_size=max_size, actual_size=size - ) + raise HTTPRequestEntityTooLarge(max_size) await self._loop.run_in_executor(None, tmp.seek, 0) if field_ct is None: @@ -782,9 +780,7 @@ async def post(self) -> "MultiDictProxy[str | bytes | FileField]": while chunk := await field.read_chunk(): size += len(chunk) if 0 < max_size < size: - raise HTTPRequestEntityTooLarge( - max_size=max_size, actual_size=size - ) + raise HTTPRequestEntityTooLarge(max_size) raw_data.extend(chunk) value = bytearray() diff --git a/aiohttp/web_ws.py b/aiohttp/web_ws.py index 4c540eea422..74811fdd283 100644 --- a/aiohttp/web_ws.py +++ b/aiohttp/web_ws.py @@ -380,7 +380,7 @@ def _post_start( loop = self._loop assert loop is not None - self._reader = WebSocketDataQueue(request._protocol, 2**16, loop=loop) + self._reader = WebSocketDataQueue(request._protocol, 2**18, loop=loop) parser = WebSocketReader( self._reader, self._max_msg_size, diff --git a/setup.cfg b/setup.cfg index 5f02a07c14f..01e43450d95 100644 --- a/setup.cfg +++ b/setup.cfg @@ -86,6 +86,8 @@ filterwarnings = # https://github.com/spulec/freezegun/issues/508 # https://github.com/spulec/freezegun/pull/511 ignore:datetime.*utcnow\(\) is deprecated and scheduled for removal:DeprecationWarning:freezegun.api + # Weird issue in Python 3.13+ triggered in test_multipart.py + ignore:coroutine method 'aclose' of 'BodyPartReader._decode_content_async' was never awaited:RuntimeWarning junit_suite_name = aiohttp_test_suite norecursedirs = dist docs build .tox .eggs minversion = 3.8.2 diff --git a/tests/test_base_protocol.py b/tests/test_base_protocol.py index 4866ea37576..20b9a41d788 100644 --- a/tests/test_base_protocol.py +++ b/tests/test_base_protocol.py @@ -5,6 +5,7 @@ import pytest from aiohttp.base_protocol import BaseProtocol +from aiohttp.http_parser import HttpParser async def test_loop() -> None: @@ -26,33 +27,28 @@ async def test_pause_writing() -> None: async def test_pause_reading_no_transport() -> None: loop = asyncio.get_event_loop() - pr = BaseProtocol(loop) - assert not pr._reading_paused + parser = mock.create_autospec(HttpParser, spec_set=True, instance=True) + pr = BaseProtocol(loop, parser=parser) pr.pause_reading() - assert not pr._reading_paused + parser.pause_reading.assert_called_once() async def test_pause_reading_stub_transport() -> None: loop = asyncio.get_event_loop() - pr = BaseProtocol(loop) + parser = mock.create_autospec(HttpParser, spec_set=True, instance=True) + pr = BaseProtocol(loop, parser=parser) tr = asyncio.Transport() pr.transport = tr assert not pr._reading_paused pr.pause_reading() assert pr._reading_paused - - -async def test_resume_reading_no_transport() -> None: - loop = asyncio.get_event_loop() - pr = BaseProtocol(loop) - pr._reading_paused = True - pr.resume_reading() - assert pr._reading_paused + parser.pause_reading.assert_called_once() # type: ignore[unreachable] async def test_resume_reading_stub_transport() -> None: loop = asyncio.get_event_loop() - pr = BaseProtocol(loop) + parser = mock.create_autospec(HttpParser, spec_set=True, instance=True) + pr = BaseProtocol(loop, parser=parser) tr = asyncio.Transport() pr.transport = tr pr._reading_paused = True diff --git a/tests/test_benchmarks_http_websocket.py b/tests/test_benchmarks_http_websocket.py index d2761b3687e..f40db171919 100644 --- a/tests/test_benchmarks_http_websocket.py +++ b/tests/test_benchmarks_http_websocket.py @@ -36,8 +36,8 @@ def test_read_one_hundred_websocket_text_messages( loop: asyncio.AbstractEventLoop, benchmark: BenchmarkFixture ) -> None: """Benchmark reading 100 WebSocket text messages.""" - queue = WebSocketDataQueue(BaseProtocol(loop), 2**16, loop=loop) - reader = WebSocketReader(queue, max_msg_size=2**16) + queue = WebSocketDataQueue(BaseProtocol(loop), 2**18, loop=loop) + reader = WebSocketReader(queue, max_msg_size=2**18) raw_message = ( b'\x81~\x01!{"id":1,"src":"shellyplugus-c049ef8c30e4","dst":"aios-1453812500' b'8","result":{"name":null,"id":"shellyplugus-c049ef8c30e4","mac":"C049EF8C30E' diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 524b712d4b8..05e6574b89f 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -53,7 +53,6 @@ from aiohttp.client_reqrep import ClientRequest from aiohttp.compression_utils import DEFAULT_MAX_DECOMPRESS_SIZE from aiohttp.connector import Connection -from aiohttp.http_exceptions import DecompressSizeError from aiohttp.http_writer import StreamWriter from aiohttp.payload import ( AsyncIterablePayload, @@ -2454,10 +2453,9 @@ async def test_payload_decompress_size_limit(aiohttp_client: AiohttpClient) -> N When a compressed payload expands beyond the configured limit, we raise DecompressSizeError. """ - # Create a highly compressible payload that exceeds the decompression limit. - # 64MiB of repeated bytes compresses to ~32KB but expands beyond the - # 32MiB per-call limit. - original = b"A" * (64 * 2**20) + # Create a highly compressible payload. + payload_size = 64 * 2**20 + original = b"A" * payload_size compressed = zlib.compress(original) assert len(original) > DEFAULT_MAX_DECOMPRESS_SIZE @@ -2474,11 +2472,11 @@ async def handler(request: web.Request) -> web.Response: async with client.get("/") as resp: assert resp.status == 200 - with pytest.raises(aiohttp.ClientPayloadError) as exc_info: - await resp.read() + received = 0 + async for chunk in resp.content.iter_chunked(1024): + received += len(chunk) - assert isinstance(exc_info.value.__cause__, DecompressSizeError) - assert "Decompressed data exceeds" in str(exc_info.value.__cause__) + assert received == payload_size @pytest.mark.skipif(brotli is None, reason="brotli is not installed") @@ -2487,8 +2485,9 @@ async def test_payload_decompress_size_limit_brotli( ) -> None: """Test that brotli decompression size limit triggers DecompressSizeError.""" assert brotli is not None - # Create a highly compressible payload that exceeds the decompression limit. - original = b"A" * (64 * 2**20) + # Create a highly compressible payload + payload_size = 64 * 2**20 + original = b"A" * payload_size compressed = brotli.compress(original) assert len(original) > DEFAULT_MAX_DECOMPRESS_SIZE @@ -2504,11 +2503,11 @@ async def handler(request: web.Request) -> web.Response: async with client.get("/") as resp: assert resp.status == 200 - with pytest.raises(aiohttp.ClientPayloadError) as exc_info: - await resp.read() + received = 0 + async for chunk in resp.content.iter_chunked(1024): + received += len(chunk) - assert isinstance(exc_info.value.__cause__, DecompressSizeError) - assert "Decompressed data exceeds" in str(exc_info.value.__cause__) + assert received == payload_size @pytest.mark.skipif(ZstdCompressor is None, reason="backports.zstd is not installed") @@ -2517,8 +2516,9 @@ async def test_payload_decompress_size_limit_zstd( ) -> None: """Test that zstd decompression size limit triggers DecompressSizeError.""" assert ZstdCompressor is not None - # Create a highly compressible payload that exceeds the decompression limit. - original = b"A" * (64 * 2**20) + # Create a highly compressible payload. + payload_size = 64 * 2**20 + original = b"A" * payload_size compressor = ZstdCompressor() compressed = compressor.compress(original) + compressor.flush() assert len(original) > DEFAULT_MAX_DECOMPRESS_SIZE @@ -2535,11 +2535,11 @@ async def handler(request: web.Request) -> web.Response: async with client.get("/") as resp: assert resp.status == 200 - with pytest.raises(aiohttp.ClientPayloadError) as exc_info: - await resp.read() + received = 0 + async for chunk in resp.content.iter_chunked(1024): + received += len(chunk) - assert isinstance(exc_info.value.__cause__, DecompressSizeError) - assert "Decompressed data exceeds" in str(exc_info.value.__cause__) + assert received == payload_size async def test_bad_payload_chunked_encoding(aiohttp_client: AiohttpClient) -> None: diff --git a/tests/test_client_proto.py b/tests/test_client_proto.py index b75ebae1137..2936aab021a 100644 --- a/tests/test_client_proto.py +++ b/tests/test_client_proto.py @@ -8,6 +8,7 @@ from aiohttp.client_proto import ResponseHandler from aiohttp.client_reqrep import ClientResponse from aiohttp.helpers import TimerNoop +from aiohttp.http_parser import HttpParser async def test_force_close(loop: asyncio.AbstractEventLoop) -> None: @@ -31,8 +32,10 @@ async def test_oserror(loop: asyncio.AbstractEventLoop) -> None: assert isinstance(proto.exception(), ClientOSError) -async def test_pause_resume_on_error(loop) -> None: +async def test_pause_resume_on_error(loop: asyncio.AbstractEventLoop) -> None: + parser = mock.create_autospec(HttpParser, spec_set=True, instance=True) proto = ResponseHandler(loop=loop) + proto._parser = parser transport = mock.Mock() proto.connection_made(transport) diff --git a/tests/test_flowcontrol_streams.py b/tests/test_flowcontrol_streams.py index 9874cc2511e..e71107b0e00 100644 --- a/tests/test_flowcontrol_streams.py +++ b/tests/test_flowcontrol_streams.py @@ -5,6 +5,7 @@ from aiohttp import streams from aiohttp.base_protocol import BaseProtocol +from aiohttp.http_parser import HttpParser @pytest.fixture @@ -43,7 +44,6 @@ async def test_readline(self, stream) -> None: stream.feed_data(b"d\n", 5) res = await stream.readline() assert res == b"d\n" - assert not stream._protocol.resume_reading.called async def test_readline_resume_paused(self, stream) -> None: stream._protocol._reading_paused = True @@ -56,7 +56,6 @@ async def test_readany(self, stream) -> None: stream.feed_data(b"data", 4) res = await stream.readany() assert res == b"data" - assert not stream._protocol.resume_reading.called async def test_readany_resume_paused(self, stream) -> None: stream._protocol._reading_paused = True @@ -70,7 +69,6 @@ async def test_readchunk(self, stream) -> None: res, end_of_http_chunk = await stream.readchunk() assert res == b"data" assert not end_of_http_chunk - assert not stream._protocol.resume_reading.called async def test_readchunk_resume_paused(self, stream) -> None: stream._protocol._reading_paused = True @@ -194,7 +192,8 @@ async def test_flow_control_data_queue_read_eof( async def test_stream_reader_eof_when_full() -> None: loop = asyncio.get_event_loop() - protocol = BaseProtocol(loop=loop) + parser = mock.create_autospec(HttpParser, spec_set=True, instance=True) + protocol = BaseProtocol(loop=loop, parser=parser) protocol.transport = asyncio.Transport() stream = streams.StreamReader(protocol, 1024, loop=loop) diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index 16e0ab1f558..6f9caa2fe55 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -1,9 +1,11 @@ # Tests for aiohttp/protocol.py import asyncio +import platform import re import sys import zlib +from collections.abc import Iterator from contextlib import nullcontext from typing import Any from unittest import mock @@ -16,6 +18,7 @@ import aiohttp from aiohttp import http_exceptions, streams from aiohttp.base_protocol import BaseProtocol +from aiohttp.client_proto import ResponseHandler from aiohttp.http_parser import ( NO_EXTENSIONS, DeflateBuffer, @@ -25,8 +28,11 @@ HttpRequestParserPy, HttpResponseParser, HttpResponseParserPy, - HttpVersion, + PayloadState, ) +from aiohttp.http_writer import HttpVersion +from aiohttp.web_protocol import RequestHandler +from aiohttp.web_server import Server try: try: @@ -57,8 +63,22 @@ @pytest.fixture -def protocol(): - return mock.Mock() +def server() -> Any: + return mock.create_autospec( + Server, + request_factory=mock.Mock(), + request_handler=mock.AsyncMock(), + instance=True, + ) + + +@pytest.fixture +def protocol() -> Any: + return mock.create_autospec( + BaseProtocol, + spec_set=True, + instance=True, + ) def _gen_ids(parsers: list[Any]) -> list[str]: @@ -69,16 +89,26 @@ def _gen_ids(parsers: list[Any]) -> list[str]: @pytest.fixture(params=REQUEST_PARSERS, ids=_gen_ids(REQUEST_PARSERS)) -def parser(loop: Any, protocol: Any, request: Any): +def parser( + loop: asyncio.AbstractEventLoop, + server: Server, + request: pytest.FixtureRequest, +) -> Iterator[HttpRequestParser]: + protocol = RequestHandler(server, loop=loop) + # Parser implementations - return request.param( + parser = request.param( protocol, loop, - 2**16, + 2**18, max_line_size=8190, max_headers=128, max_field_size=8190, ) + protocol._force_close = False + protocol._parser = parser + with mock.patch.object(protocol, "transport", True): + yield parser @pytest.fixture(params=REQUEST_PARSERS, ids=_gen_ids(REQUEST_PARSERS)) @@ -88,17 +118,24 @@ def request_cls(request: Any): @pytest.fixture(params=RESPONSE_PARSERS, ids=_gen_ids(RESPONSE_PARSERS)) -def response(loop: Any, protocol: Any, request: Any): +def response( + loop: asyncio.AbstractEventLoop, + request: pytest.FixtureRequest, +) -> HttpResponseParser: + protocol = ResponseHandler(loop) + # Parser implementations - return request.param( + parser = request.param( protocol, loop, - 2**16, + 2**18, max_line_size=8190, max_headers=128, max_field_size=8190, read_until_eof=True, ) + protocol._parser = parser + return parser # type: ignore[no-any-return] @pytest.fixture(params=RESPONSE_PARSERS, ids=_gen_ids(RESPONSE_PARSERS)) @@ -149,7 +186,13 @@ def test_reject_obsolete_line_folding(parser: Any) -> None: @pytest.mark.skipif(NO_EXTENSIONS, reason="Only tests C parser.") -def test_invalid_character(loop: Any, protocol: Any, request: Any) -> None: +def test_invalid_character( + loop: asyncio.AbstractEventLoop, + server: Server, + request: pytest.FixtureRequest, +) -> None: + protocol = RequestHandler(server, loop=loop) + parser = HttpRequestParserC( protocol, loop, @@ -157,6 +200,7 @@ def test_invalid_character(loop: Any, protocol: Any, request: Any) -> None: max_line_size=8190, max_field_size=8190, ) + protocol._parser = parser text = b"POST / HTTP/1.1\r\nHost: localhost:8080\r\nSet-Cookie: abc\x01def\r\n\r\n" error_detail = re.escape( r""": @@ -169,7 +213,13 @@ def test_invalid_character(loop: Any, protocol: Any, request: Any) -> None: @pytest.mark.skipif(NO_EXTENSIONS, reason="Only tests C parser.") -def test_invalid_linebreak(loop: Any, protocol: Any, request: Any) -> None: +def test_invalid_linebreak( + loop: asyncio.AbstractEventLoop, + server: Server, + request: pytest.FixtureRequest, +) -> None: + protocol = RequestHandler(server, loop=loop) + parser = HttpRequestParserC( protocol, loop, @@ -177,6 +227,7 @@ def test_invalid_linebreak(loop: Any, protocol: Any, request: Any) -> None: max_line_size=8190, max_field_size=8190, ) + protocol._parser = parser text = b"GET /world HTTP/1.1\r\nHost: 127.0.0.1\n\r\n" error_detail = re.escape( r""": @@ -239,7 +290,11 @@ def test_ctl_host_header_bad_characters(parser: HttpRequestParser) -> None: parser.feed_data(text) -def test_unpaired_surrogate_in_header_py(loop: Any, protocol: Any) -> None: +def test_unpaired_surrogate_in_header_py( + loop: asyncio.AbstractEventLoop, server: Server +) -> None: + protocol = RequestHandler(server, loop=loop) + parser = HttpRequestParserPy( protocol, loop, @@ -247,6 +302,7 @@ def test_unpaired_surrogate_in_header_py(loop: Any, protocol: Any) -> None: max_line_size=8190, max_field_size=8190, ) + protocol._parser = parser text = b"POST / HTTP/1.1\r\n\xff\r\n\r\n" message = None try: @@ -987,6 +1043,203 @@ def test_max_header_value_size_under_limit(parser: HttpRequestParser) -> None: assert msg.url == URL("/test") +async def test_chunk_splits_after_pause(parser: HttpRequestParser) -> None: + text = ( + b"GET /test HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n" + + b"1\r\nb\r\n" * 50000 + + b"0\r\n\r\n" + ) + + messages, upgrade, tail = parser.feed_data(text) + payload = messages[0][-1] + # Payload should have paused reading and stopped receiving new chunks after 16k. + assert payload._http_chunk_splits is not None + assert len(payload._http_chunk_splits) == 16385 + # We should still get the full result after read(), as it will continue processing. + result = await payload.read() + assert len(result) == 50000 # Compare len first, as it's easier to debug in diff. + assert result == b"b" * 50000 + + +async def test_compressed_with_tail(response: HttpResponseParser) -> None: + """Test compressed content-length body followed by a second response. + + With 2 responses arriving in one call and the first compressed, this should + trigger decompression pausing with the second response being saved as the tail. + Verify that the second response is resumed from the tail. + """ + # Must be large enough to exceed high water mark. + original = b"x" * 1024 * 1024 + compressed = zlib.compress(original) + resp1 = ( + b"HTTP/1.1 200 OK\r\n" + b"Content-Length: " + str(len(compressed)).encode() + b"\r\n" + b"Content-Encoding: deflate\r\n" + b"\r\n" + ) + compressed + resp2 = b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok" + + msgs, upgrade, tail = response.feed_data(resp1 + resp2) + payload = msgs[0][-1] + result = await payload.read() + assert len(result) == len(original) + assert result == original + + payload = response.protocol._buffer[0][0][-1] + result = await payload.read() + assert result == b"ok" + + +async def test_two_content_length_responses_in_one_call( + response: HttpResponseParser, +) -> None: + """Two complete responses in a single feed_data call. + + The first payload completes with tail data for the second, hitting the + PAYLOAD_COMPLETE branch that resets the parser for the next message. + """ + resp1 = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello" + resp2 = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nworld" + + msgs, upgrade, tail = response.feed_data(resp1 + resp2) + assert len(msgs) == 2 + assert await msgs[0][-1].read() == b"hello" + assert await msgs[1][-1].read() == b"world" + + +@pytest.mark.usefixtures("parametrize_zlib_backend") +async def test_compressed_zlib_64kb(response_cls: type[HttpResponseParser]) -> None: + loop = asyncio.get_running_loop() + protocol = ResponseHandler(loop) + response = response_cls( + protocol, + loop, + # 64KiB limit triggered a bug with isal implementation not returning all data. + 2**16, + max_line_size=8190, + max_headers=128, + max_field_size=8190, + ) + protocol._parser = response + + original = b"".join( + bytes((*range(0, i), *range(i, 0, -1))) for _ in range(255) for i in range(255) + ) + compressed = zlib.compress(original) + headers = ( + b"HTTP/1.1 200 OK\r\n" + b"Content-Length: " + str(len(compressed)).encode() + b"\r\n" + b"Content-Encoding: deflate\r\n" + b"\r\n" + ) + + msgs, upgrade, tail = response.feed_data(headers + compressed) + payload = msgs[0][-1] + result = await payload.read() + assert len(result) == len(original) + assert result == original + + +async def test_compressed_chunked_with_pending(response: HttpResponseParser) -> None: + """Test chunked + compressed where the decompressor needs to resume from pause. + + We need to verify that chunked messages continue parsing correctly after + a pause and resume in the decompression. + """ + # Must be large enough to exceed high water mark. + original = b"A" * 1024 * 1024 + compressed = zlib.compress(original) + chunk_data = hex(len(compressed))[2:].encode() + b"\r\n" + compressed + b"\r\n" + headers = ( + b"HTTP/1.1 200 OK\r\n" + b"Transfer-Encoding: chunked\r\n" + b"Content-Encoding: deflate\r\n" + b"\r\n" + ) + data = headers + chunk_data + b"0\r\n\r\n" + + msgs, upgrade, tail = response.feed_data(data) + payload = msgs[0][-1] + result = await payload.read() + assert len(result) == len(original) + assert result == original + + +async def test_compressed_until_eof_with_pending(response: HttpResponseParser) -> None: + """Test read-until-eof + compressed with pause.""" + # Must be large enough to exceed high water mark. + original = b"B" * 5 * 1024 * 1024 + compressed = zlib.compress(original) + # No Content-Length or Transfer-Encoding means the parser must parse until EOF. + headers = b"HTTP/1.1 200 OK\r\nContent-Encoding: deflate\r\n\r\n" + + msgs, upgrade, tail = response.feed_data(headers + compressed) + response.feed_eof() + payload = msgs[0][-1] + + # Check that .feed_eof() hasn't decompressed entire payload into memory. + assert sum(len(b) for b in payload._buffer) <= (1024 * 1024) + + result = await payload.read() + assert len(result) == len(original) + assert result == original + + +async def test_compressed_until_eof_high_water( + response_cls: type[HttpResponseParser], +) -> None: + """Test read-until-eof + compressed with higher limit.""" + loop = asyncio.get_running_loop() + protocol = ResponseHandler(loop) + response = response_cls( + protocol, + loop, + 2**19, # 512 KiB limit + max_line_size=8190, + max_headers=128, + max_field_size=8190, + read_until_eof=True, + ) + protocol._parser = response + + # Must be large enough to exceed high water mark. + original = b"B" * 5 * 1024 * 1024 + compressed = zlib.compress(original) + # No Content-Length or Transfer-Encoding means the parser must parse until EOF. + headers = b"HTTP/1.1 200 OK\r\nContent-Encoding: deflate\r\n\r\n" + + msgs, upgrade, tail = response.feed_data(headers + compressed) + response.feed_eof() + payload = msgs[0][-1] + + # Check that .feed_eof() hasn't decompressed entire payload into memory. + assert sum(len(b) for b in payload._buffer) <= (2 * 1024 * 1024) + # Individual chunks should have been decompressed at limit amount. + assert all(len(b) == 512 * 1024 for b in payload._buffer) + + result = await payload.read() + assert len(result) == len(original) + assert result == original + + +async def test_compressed_256kb(response: HttpResponseParser) -> None: + original = b"x" * 256 * 1024 + compressed = zlib.compress(original) + headers = ( + b"HTTP/1.1 200 OK\r\n" + b"Content-Length: " + str(len(compressed)).encode() + b"\r\n" + b"Content-Encoding: deflate\r\n" + b"\r\n" + ) + + messages, upgrade, tail = response.feed_data(headers + compressed) + assert len(messages) == 1 + payload = messages[0][-1] + result = await payload.read() + assert len(result) == len(original) + assert result == original + + @pytest.mark.parametrize("size", [40965, 8191]) def test_max_header_value_size_continuation(response, size) -> None: name = b"T" * (size - 5) @@ -1412,14 +1665,19 @@ async def test_http_response_parser_bad_chunked_lax(response) -> None: @pytest.mark.dev_mode -async def test_http_response_parser_bad_chunked_strict_py(loop, protocol) -> None: +async def test_http_response_parser_bad_chunked_strict_py( + loop: asyncio.AbstractEventLoop, +) -> None: + protocol = ResponseHandler(loop) + response = HttpResponseParserPy( protocol, loop, - 2**16, + 2**18, max_line_size=8190, max_field_size=8190, ) + protocol._parser = response text = ( b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5 \r\nabcde\r\n0\r\n\r\n" ) @@ -1432,7 +1690,11 @@ async def test_http_response_parser_bad_chunked_strict_py(loop, protocol) -> Non "HttpRequestParserC" not in dir(aiohttp.http_parser), reason="C based HTTP parser not available", ) -async def test_http_response_parser_bad_chunked_strict_c(loop, protocol) -> None: +async def test_http_response_parser_bad_chunked_strict_c( + loop: asyncio.AbstractEventLoop, +) -> None: + protocol = ResponseHandler(loop) + response = HttpResponseParserC( protocol, loop, @@ -1440,6 +1702,7 @@ async def test_http_response_parser_bad_chunked_strict_c(loop, protocol) -> None max_line_size=8190, max_field_size=8190, ) + protocol._parser = response text = ( b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5 \r\nabcde\r\n0\r\n\r\n" ) @@ -1582,18 +1845,25 @@ async def test_request_chunked_reject_bad_trailer(parser: HttpRequestParser) -> def test_parse_no_length_or_te_on_post( loop: asyncio.AbstractEventLoop, - protocol: BaseProtocol, + server: Server, request_cls: type[HttpRequestParser], ) -> None: - parser = request_cls(protocol, loop, limit=2**16) + protocol = RequestHandler(server, loop=loop) + parser = request_cls(protocol, loop, limit=2**18) + protocol._parser = parser text = b"POST /test HTTP/1.1\r\n\r\n" msg, payload = parser.feed_data(text)[0][0] assert payload.is_eof() -def test_parse_payload_response_without_body(loop, protocol, response_cls) -> None: +def test_parse_payload_response_without_body( + loop: asyncio.AbstractEventLoop, + response_cls: type[HttpResponseParser], +) -> None: + protocol = ResponseHandler(loop) parser = response_cls(protocol, loop, 2**16, response_with_body=False) + protocol._parser = parser text = b"HTTP/1.1 200 Ok\r\ncontent-length: 10\r\n\r\n" msg, payload = parser.feed_data(text)[0][0] @@ -1849,16 +2119,21 @@ def test_parse_uri_utf8_percent_encoded(parser) -> None: "HttpRequestParserC" not in dir(aiohttp.http_parser), reason="C based HTTP parser not available", ) -def test_parse_bad_method_for_c_parser_raises(loop, protocol): +def test_parse_bad_method_for_c_parser_raises( + loop: asyncio.AbstractEventLoop, server: Server +) -> None: + protocol = RequestHandler(server, loop=loop) + payload = b"GET1 /test HTTP/1.1\r\n\r\n" parser = HttpRequestParserC( protocol, loop, - 2**16, + 2**18, max_line_size=8190, max_headers=128, max_field_size=8190, ) + protocol._parser = parser with pytest.raises(aiohttp.http_exceptions.BadStatusLine): messages, upgrade, tail = parser.feed_data(payload) @@ -1875,7 +2150,7 @@ async def test_parse_eof_payload(self, protocol: BaseProtocol) -> None: assert [(bytearray(b"data"))] == list(out._buffer) async def test_parse_length_payload_eof(self, protocol: BaseProtocol) -> None: - out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) p = HttpPayloadParser(out, length=4, headers_parser=HeadersParser()) p.feed_data(b"da") @@ -1899,7 +2174,7 @@ async def test_parse_chunked_payload_size_data_mismatch( Regression test for #10596. """ - out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser()) # Declared chunk-size is 4 but actual data is "Hello" (5 bytes). # After consuming 4 bytes, remaining starts with "o" not "\r\n". @@ -1914,7 +2189,7 @@ async def test_parse_chunked_payload_size_data_mismatch_too_short( Regression test for #10596. """ - out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser()) # Declared chunk-size is 6 but actual data before CRLF is "Hello" (5 bytes). # Parser reads 6 bytes: "Hello\r", then expects \r\n but sees "\n0\r\n..." @@ -1936,7 +2211,7 @@ async def test_parse_chunked_payload_split_end( async def test_parse_chunked_payload_split_end2( self, protocol: BaseProtocol ) -> None: - out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser()) p.feed_data(b"4\r\nasdf\r\n0\r\n\r") p.feed_data(b"\n") @@ -1947,7 +2222,7 @@ async def test_parse_chunked_payload_split_end2( async def test_parse_chunked_payload_split_end_trailers( self, protocol: BaseProtocol ) -> None: - out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser()) p.feed_data(b"4\r\nasdf\r\n0\r\n") p.feed_data(b"Content-MD5: 912ec803b2ce49e4a541068d495ab570\r\n") @@ -1959,7 +2234,7 @@ async def test_parse_chunked_payload_split_end_trailers( async def test_parse_chunked_payload_split_end_trailers2( self, protocol: BaseProtocol ) -> None: - out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser()) p.feed_data(b"4\r\nasdf\r\n0\r\n") p.feed_data(b"Content-MD5: 912ec803b2ce49e4a541068d495ab570\r\n\r") @@ -1991,10 +2266,10 @@ async def test_parse_chunked_payload_split_end_trailers4( assert b"asdf" == b"".join(out._buffer) async def test_http_payload_parser_length(self, protocol: BaseProtocol) -> None: - out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) p = HttpPayloadParser(out, length=2, headers_parser=HeadersParser()) - eof, tail = p.feed_data(b"1245") - assert eof + state, tail = p.feed_data(b"1245") + assert state is PayloadState.PAYLOAD_COMPLETE assert b"12" == out._buffer[0] assert b"45" == tail @@ -2004,7 +2279,7 @@ async def test_http_payload_parser_deflate(self, protocol: BaseProtocol) -> None COMPRESSED = b"x\x9cKI,I\x04\x00\x04\x00\x01\x9b" length = len(COMPRESSED) - out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) p = HttpPayloadParser( out, length=length, compression="deflate", headers_parser=HeadersParser() ) @@ -2075,7 +2350,7 @@ async def test_http_payload_parser_deflate_split_err( async def test_http_payload_parser_length_zero( self, protocol: BaseProtocol ) -> None: - out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) p = HttpPayloadParser(out, length=0, headers_parser=HeadersParser()) assert p.done assert out.is_eof() @@ -2083,7 +2358,7 @@ async def test_http_payload_parser_length_zero( @pytest.mark.skipif(brotli is None, reason="brotli is not installed") async def test_http_payload_brotli(self, protocol: BaseProtocol) -> None: compressed = brotli.compress(b"brotli data") - out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) p = HttpPayloadParser( out, length=len(compressed), @@ -2097,7 +2372,7 @@ async def test_http_payload_brotli(self, protocol: BaseProtocol) -> None: @pytest.mark.skipif(zstandard is None, reason="zstandard is not installed") async def test_http_payload_zstandard(self, protocol: BaseProtocol) -> None: compressed = zstandard.compress(b"zstd data") - out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) p = HttpPayloadParser( out, length=len(compressed), @@ -2115,7 +2390,7 @@ async def test_http_payload_zstandard_multi_frame( frame1 = zstandard.compress(b"first") frame2 = zstandard.compress(b"second") payload = frame1 + frame2 - out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) p = HttpPayloadParser( out, length=len(payload), @@ -2132,7 +2407,7 @@ async def test_http_payload_zstandard_multi_frame_chunked( ) -> None: frame1 = zstandard.compress(b"chunk1") frame2 = zstandard.compress(b"chunk2") - out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) p = HttpPayloadParser( out, length=len(frame1) + len(frame2), @@ -2152,7 +2427,7 @@ async def test_http_payload_zstandard_frame_split_mid_chunk( frame2 = zstandard.compress(b"BBBB") combined = frame1 + frame2 split_point = len(frame1) + 3 # 3 bytes into frame2 - out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) p = HttpPayloadParser( out, length=len(combined), @@ -2170,7 +2445,7 @@ async def test_http_payload_zstandard_many_small_frames( ) -> None: parts = [f"part{i}".encode() for i in range(10)] payload = b"".join(zstandard.compress(p) for p in parts) - out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) p = HttpPayloadParser( out, length=len(payload), @@ -2184,7 +2459,7 @@ async def test_http_payload_zstandard_many_small_frames( class TestDeflateBuffer: async def test_feed_data(self, protocol: BaseProtocol) -> None: - buf = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_event_loop()) + buf = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) dbuf = DeflateBuffer(buf, "deflate") dbuf.decompressor = mock.Mock() @@ -2212,10 +2487,10 @@ async def test_feed_eof(self, protocol: BaseProtocol) -> None: dbuf = DeflateBuffer(buf, "deflate") dbuf.decompressor = mock.Mock() - dbuf.decompressor.flush.return_value = b"line" + dbuf.decompressor.data_available = False + dbuf.decompressor.flush.return_value = b"" dbuf.feed_eof() - assert [b"line"] == list(buf._buffer) assert buf._eof async def test_feed_eof_err_deflate(self, protocol: BaseProtocol) -> None: @@ -2223,8 +2498,10 @@ async def test_feed_eof_err_deflate(self, protocol: BaseProtocol) -> None: dbuf = DeflateBuffer(buf, "deflate") dbuf.decompressor = mock.Mock() - dbuf.decompressor.flush.return_value = b"line" + dbuf.decompressor.data_available = False + dbuf.decompressor.flush.return_value = b"" dbuf.decompressor.eof = False + dbuf.size = 1 # Simulate that data was previously fed with pytest.raises(http_exceptions.ContentEncodingError): dbuf.feed_eof() @@ -2234,22 +2511,24 @@ async def test_feed_eof_no_err_gzip(self, protocol: BaseProtocol) -> None: dbuf = DeflateBuffer(buf, "gzip") dbuf.decompressor = mock.Mock() - dbuf.decompressor.flush.return_value = b"line" + dbuf.decompressor.data_available = False + dbuf.decompressor.flush.return_value = b"" dbuf.decompressor.eof = False dbuf.feed_eof() - assert [b"line"] == list(buf._buffer) + assert buf._eof async def test_feed_eof_no_err_brotli(self, protocol: BaseProtocol) -> None: buf = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) dbuf = DeflateBuffer(buf, "br") dbuf.decompressor = mock.Mock() - dbuf.decompressor.flush.return_value = b"line" + dbuf.decompressor.data_available = False + dbuf.decompressor.flush.return_value = b"" dbuf.decompressor.eof = False dbuf.feed_eof() - assert [b"line"] == list(buf._buffer) + assert buf._eof @pytest.mark.skipif(zstandard is None, reason="zstandard is not installed") async def test_feed_eof_no_err_zstandard(self, protocol: BaseProtocol) -> None: @@ -2257,19 +2536,21 @@ async def test_feed_eof_no_err_zstandard(self, protocol: BaseProtocol) -> None: dbuf = DeflateBuffer(buf, "zstd") dbuf.decompressor = mock.Mock() - dbuf.decompressor.flush.return_value = b"line" + dbuf.decompressor.data_available = False + dbuf.decompressor.flush.return_value = b"" dbuf.decompressor.eof = False dbuf.feed_eof() - assert [b"line"] == list(buf._buffer) + assert buf._eof async def test_empty_body(self, protocol: BaseProtocol) -> None: - buf = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + buf = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) dbuf = DeflateBuffer(buf, "deflate") dbuf.feed_eof() assert buf.at_eof() + @pytest.mark.skipif(platform.python_implementation() == "PyPy", reason="Broken") @pytest.mark.parametrize( "chunk_size", [1024, 2**14, 2**16], # 1KB, 16KB, 64KB @@ -2288,13 +2569,14 @@ async def test_streaming_decompress_large_payload( original = b"A" * (3 * 2**20) compressed = zlib.compress(original) - buf = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + buf = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) dbuf = DeflateBuffer(buf, "deflate") # Feed compressed data in chunks (simulating network streaming) for i in range(0, len(compressed), chunk_size): # pragma: no branch chunk = compressed[i : i + chunk_size] - dbuf.feed_data(chunk, len(chunk)) + while dbuf.feed_data(chunk, len(chunk)): + chunk = b"" dbuf.feed_eof() diff --git a/tests/test_http_writer.py b/tests/test_http_writer.py index 0032f040cf5..251645f2398 100644 --- a/tests/test_http_writer.py +++ b/tests/test_http_writer.py @@ -1394,7 +1394,7 @@ async def test_write_drain_condition_with_small_buffer( protocol._drain_helper.reset_mock() # type: ignore[attr-defined] # Write small amount of data with drain=True but buffer under limit - small_data = b"x" * 100 # Much less than LIMIT (2**16) + small_data = b"x" * 100 # Much less than LIMIT (2**18) await msg.write(small_data, drain=True) # Drain should NOT be called because buffer_size <= LIMIT @@ -1423,7 +1423,7 @@ async def test_write_drain_condition_with_large_buffer( protocol._drain_helper.reset_mock() # type: ignore[attr-defined] # Write large amount of data with drain=True - large_data = b"x" * (2**16 + 1) # Just over LIMIT + large_data = b"x" * (2**18 + 1) # Just over LIMIT await msg.write(large_data, drain=True) # Drain should be called because drain=True AND buffer_size > LIMIT @@ -1452,12 +1452,12 @@ async def test_write_no_drain_with_large_buffer( protocol._drain_helper.reset_mock() # type: ignore[attr-defined] # Write large amount of data with drain=False - large_data = b"x" * (2**16 + 1) # Just over LIMIT + large_data = b"x" * (2**18 + 1) # Just over LIMIT await msg.write(large_data, drain=False) # Drain should NOT be called because drain=False assert not protocol._drain_helper.called # type: ignore[attr-defined] - assert msg.buffer_size == (2**16 + 1) # Buffer not reset + assert msg.buffer_size == (2**18 + 1) # Buffer not reset assert large_data in buf diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 34e745830a9..0d01bf6a5ae 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -1,7 +1,9 @@ import asyncio +import gzip import io import json import pathlib +import sys from unittest import mock import pytest @@ -24,6 +26,7 @@ MultipartResponseWrapper, ) from aiohttp.streams import StreamReader +from aiohttp.web_exceptions import HTTPRequestEntityTooLarge BOUNDARY = b"--:" @@ -345,17 +348,17 @@ async def test_read_with_content_encoding_gzip(self) -> None: result = await obj.read(decode=True) assert b"Time to Relax!" == result + @pytest.mark.skipif(sys.version_info < (3, 11), reason="wbits not available") async def test_read_with_content_encoding_deflate(self) -> None: - data = b"\x0b\xc9\xccMU(\xc9W\x08J\xcdI\xacP\x04\x00" - tail = b"%s--:--" % newline - with Stream(data + tail) as stream: - obj = aiohttp.BodyPartReader( - BOUNDARY, - {CONTENT_ENCODING: "deflate"}, - stream, - ) + content = b"A" * 1_000_000 # Large enough to exceed max_length. + compressed = ZLibBackend.compress(content, wbits=-ZLibBackend.MAX_WBITS) + + h = CIMultiDictProxy(CIMultiDict({CONTENT_ENCODING: "deflate"})) + with Stream(compressed + b"\r\n--:--") as stream: + obj = aiohttp.BodyPartReader(BOUNDARY, h, stream) result = await obj.read(decode=True) - assert b"Time to Relax!" == result + assert len(result) == len(content) # Simplifies diff on failure + assert result == content async def test_read_with_content_encoding_identity(self) -> None: thing = ( @@ -381,6 +384,22 @@ async def test_read_with_content_encoding_unknown(self) -> None: with pytest.raises(RuntimeError): await obj.read(decode=True) + async def test_read_decode_compressed_exceeds_max_size(self) -> None: + # Compressed data is small, but decompresses beyond client_max_size. + original = b"A" * 1024 + compressed = gzip.compress(original) + h = CIMultiDictProxy(CIMultiDict({CONTENT_ENCODING: "gzip"})) + with Stream(compressed + b"\r\n--:--") as stream: + obj = aiohttp.BodyPartReader( + BOUNDARY, + h, + stream, + client_max_size=256, + max_size_error_cls=HTTPRequestEntityTooLarge, + ) + with pytest.raises(HTTPRequestEntityTooLarge): + await obj.read(decode=True) + async def test_read_with_content_transfer_encoding_base64(self) -> None: with Stream(b"VGltZSB0byBSZWxheCE=%s--:--" % newline) as stream: obj = aiohttp.BodyPartReader( @@ -720,9 +739,9 @@ async def test_filename(self) -> None: assert "foo.html" == part.filename async def test_reading_long_part(self) -> None: - size = 2 * 2**16 + size = 2 * 2**18 protocol = mock.Mock(_reading_paused=False) - stream = StreamReader(protocol, 2**16, loop=asyncio.get_event_loop()) + stream = StreamReader(protocol, 2**18, loop=asyncio.get_event_loop()) stream.feed_data(b"0" * size + b"\r\n--:--") stream.feed_eof() obj = aiohttp.BodyPartReader(BOUNDARY, {}, stream) @@ -1805,6 +1824,35 @@ async def test_body_part_reader_payload_as_bytes() -> None: payload.decode() +@pytest.mark.skipif(sys.version_info < (3, 11), reason="No wbits parameter") +async def test_body_part_reader_payload_write() -> None: + content = b"A" * 1_000_000 # Large enough to exceed max_length. + compressed = ZLibBackend.compress(content, wbits=-ZLibBackend.MAX_WBITS) + output = b"" + + async def write(inp: bytes) -> None: + nonlocal output + output += inp + + h = CIMultiDictProxy(CIMultiDict({CONTENT_ENCODING: "deflate"})) + if sys.version_info >= (3, 12): + writer = mock.create_autospec( + AbstractStreamWriter, write=write, spec_set=True, instance=True + ) + else: + writer = mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ) + writer.write.side_effect = write + with Stream(compressed + b"\r\n--:--") as stream: + body_part = aiohttp.BodyPartReader(BOUNDARY, h, stream) + payload = BodyPartReaderPayload(body_part) + await payload.write(writer) + + assert len(output) == len(content) # Simplifies diff on failure + assert output == content + + async def test_multipart_writer_close_with_exceptions() -> None: """Test that MultipartWriter.close() continues closing all parts even if one raises.""" writer = aiohttp.MultipartWriter() diff --git a/tests/test_payload.py b/tests/test_payload.py index 6fcc00449b8..f5454ec46f1 100644 --- a/tests/test_payload.py +++ b/tests/test_payload.py @@ -331,7 +331,7 @@ def mock_read(size: int | None = None) -> bytes: async def test_bytesio_payload_large_data_multiple_chunks() -> None: """Test BytesIOPayload with large data requiring multiple read chunks.""" - chunk_size = 2**16 # 64KB (READ_SIZE) + chunk_size = 2**18 # 256KiB (READ_SIZE) data = b"x" * (chunk_size + 1000) # Slightly larger than READ_SIZE payload_bytesio = payload.BytesIOPayload(io.BytesIO(data)) writer = MockStreamWriter() @@ -355,7 +355,7 @@ async def test_bytesio_payload_remaining_bytes_exhausted() -> None: async def test_iobase_payload_exact_chunk_size_limit() -> None: """Test IOBasePayload with content length matching exactly one read chunk.""" - chunk_size = 2**16 # 65536 bytes (READ_SIZE) + chunk_size = 2**18 # 256KiB (READ_SIZE) data = b"x" * chunk_size + b"extra" # Slightly larger than one read chunk p = payload.IOBasePayload(io.BytesIO(data)) writer = MockStreamWriter() diff --git a/tests/test_streams.py b/tests/test_streams.py index 93686746ee0..a16b991e0ce 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -25,7 +25,7 @@ def chunkify(seq, n): async def create_stream(): loop = asyncio.get_event_loop() protocol = mock.Mock(_reading_paused=False) - stream = streams.StreamReader(protocol, 2**16, loop=loop) + stream = streams.StreamReader(protocol, 2**18, loop=loop) stream.feed_data(DATA) stream.feed_eof() return stream @@ -73,7 +73,7 @@ class TestStreamReader: DATA = b"line1\nline2\nline3\n" def _make_one(self, *args, **kwargs): - kwargs.setdefault("limit", 2**16) + kwargs.setdefault("limit", 2**18) return streams.StreamReader(mock.Mock(_reading_paused=False), *args, **kwargs) async def test_create_waiter(self) -> None: @@ -1131,7 +1131,7 @@ async def test_empty_stream_reader() -> None: assert s.set_exception(ValueError()) is None assert s.exception() is None assert s.feed_eof() is None - assert s.feed_data(b"data") is None + assert s.feed_data(b"data") is False assert s.at_eof() assert (await s.wait_eof()) is None assert await s.read() == b"" @@ -1293,7 +1293,7 @@ async def set_err(): async def test_feed_data_waiters(protocol) -> None: loop = asyncio.get_event_loop() - reader = streams.StreamReader(protocol, 2**16, loop=loop) + reader = streams.StreamReader(protocol, 2**18, loop=loop) waiter = reader._waiter = loop.create_future() eof_waiter = reader._eof_waiter = loop.create_future() @@ -1321,7 +1321,7 @@ async def test_feed_data_completed_waiters(protocol) -> None: async def test_feed_eof_waiters(protocol) -> None: loop = asyncio.get_event_loop() - reader = streams.StreamReader(protocol, 2**16, loop=loop) + reader = streams.StreamReader(protocol, 2**18, loop=loop) waiter = reader._waiter = loop.create_future() eof_waiter = reader._eof_waiter = loop.create_future() @@ -1353,7 +1353,7 @@ async def test_feed_eof_cancelled(protocol) -> None: async def test_on_eof(protocol) -> None: loop = asyncio.get_event_loop() - reader = streams.StreamReader(protocol, 2**16, loop=loop) + reader = streams.StreamReader(protocol, 2**18, loop=loop) on_eof = mock.Mock() reader.on_eof(on_eof) @@ -1374,7 +1374,7 @@ async def test_on_eof_empty_reader() -> None: async def test_on_eof_exc_in_callback(protocol) -> None: loop = asyncio.get_event_loop() - reader = streams.StreamReader(protocol, 2**16, loop=loop) + reader = streams.StreamReader(protocol, 2**18, loop=loop) on_eof = mock.Mock() on_eof.side_effect = ValueError @@ -1409,7 +1409,7 @@ async def test_on_eof_eof_is_set(protocol) -> None: async def test_on_eof_eof_is_set_exception(protocol) -> None: loop = asyncio.get_event_loop() - reader = streams.StreamReader(protocol, 2**16, loop=loop) + reader = streams.StreamReader(protocol, 2**18, loop=loop) reader.feed_eof() on_eof = mock.Mock() @@ -1455,7 +1455,7 @@ async def test_set_exception_cancelled(protocol) -> None: async def test_set_exception_eof_callbacks(protocol) -> None: loop = asyncio.get_event_loop() - reader = streams.StreamReader(protocol, 2**16, loop=loop) + reader = streams.StreamReader(protocol, 2**18, loop=loop) on_eof = mock.Mock() reader.on_eof(on_eof) @@ -1560,8 +1560,8 @@ async def test_stream_reader_pause_on_high_water_chunks( ) -> None: """Test that reading is paused when chunk count exceeds high water mark.""" loop = asyncio.get_event_loop() - # Use small limit so high_water_chunks is small: limit // 4 = 10 - stream = streams.StreamReader(protocol, limit=40, loop=loop) + # Use small limit so high_water_chunks is small: limit // 16 = 10 + stream = streams.StreamReader(protocol, limit=160, loop=loop) assert stream._high_water_chunks == 10 assert stream._low_water_chunks == 5 @@ -1581,8 +1581,8 @@ async def test_stream_reader_resume_on_low_water_chunks( ) -> None: """Test that reading resumes when chunk count drops below low water mark.""" loop = asyncio.get_event_loop() - # Use small limit so high_water_chunks is small: limit // 4 = 10 - stream = streams.StreamReader(protocol, limit=40, loop=loop) + # Use small limit so high_water_chunks is small: limit // 16 = 10 + stream = streams.StreamReader(protocol, limit=160, loop=loop) assert stream._high_water_chunks == 10 assert stream._low_water_chunks == 5 @@ -1676,14 +1676,14 @@ async def test_stream_reader_resume_non_chunked_when_paused( protocol.resume_reading.assert_called() -@pytest.mark.parametrize("limit", [1, 2, 4]) +@pytest.mark.parametrize("limit", (1, 4, 7, 16)) async def test_stream_reader_small_limit_resumes_reading( protocol: mock.Mock, limit: int, ) -> None: """Test that small limits still allow resume_reading to be called. - Even with very small limits, high_water_chunks should be at least 3 + Even with very small limits, high_water_chunks should be at least 4 and low_water_chunks should be at least 2, with high > low to ensure proper flow control. """ @@ -1691,8 +1691,8 @@ async def test_stream_reader_small_limit_resumes_reading( stream = streams.StreamReader(protocol, limit=limit, loop=loop) # Verify minimum thresholds are enforced and high > low - assert stream._high_water_chunks >= 3 - assert stream._low_water_chunks >= 2 + assert stream._high_water_chunks == 4 + assert stream._low_water_chunks == 2 assert stream._high_water_chunks > stream._low_water_chunks # Set up pause/resume side effects @@ -1706,8 +1706,8 @@ def resume_reading() -> None: protocol.resume_reading.side_effect = resume_reading - # Feed 4 chunks (triggers pause at > high_water_chunks which is >= 3) - for char in b"abcd": + # Feed 5 chunks (triggers pause at > high_water_chunks which is 4) + for char in b"abcde": stream.begin_http_chunk_receiving() stream.feed_data(bytes([char])) stream.end_http_chunk_receiving() @@ -1718,7 +1718,7 @@ def resume_reading() -> None: # Read all data - should resume (chunk count drops below low_water_chunks) data = stream.read_nowait() - assert data == b"abcd" + assert data == b"abcde" assert stream._size == 0 protocol.resume_reading.assert_called() diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py index 0d9ab86f036..d429e7ceaa0 100644 --- a/tests/test_web_functional.py +++ b/tests/test_web_functional.py @@ -322,7 +322,28 @@ async def handler(request): await resp.release() -async def test_multipart_empty(aiohttp_client) -> None: +async def test_multipart_client_max_size(aiohttp_client: AiohttpClient) -> None: + with multipart.MultipartWriter() as writer: + writer.append("A" * 1020) + + async def handler(request: web.Request) -> web.Response: + reader = await request.multipart() + assert isinstance(reader, multipart.MultipartReader) + + part = await reader.next() + assert isinstance(part, multipart.BodyPartReader) + await part.text() # Should raise HttpRequestEntityTooLarge + assert False + + app = web.Application(client_max_size=1000) + app.router.add_post("/", handler) + client = await aiohttp_client(app) + + async with client.post("/", data=writer) as resp: + assert resp.status == 413 + + +async def test_multipart_empty(aiohttp_client: AiohttpClient) -> None: with multipart.MultipartWriter() as writer: pass @@ -1698,12 +1719,9 @@ async def handler(request): resp = await client.post("/", data=data) assert 413 == resp.status resp_text = await resp.text() - assert "Maximum request body size 1048576 exceeded, actual body size" in resp_text - # Maximum request body size X exceeded, actual body size X - body_size = int(resp_text.split()[-1]) - assert body_size >= max_size + assert "Maximum request body size 1048576 exceeded" in resp_text - await resp.release() + resp.release() async def test_app_max_client_size_adjusted(aiohttp_client: AiohttpClient) -> None: @@ -1730,10 +1748,7 @@ async def handler(request: web.Request) -> web.Response: resp = await client.post("/", data=too_large_data) assert 413 == resp.status resp_text = await resp.text() - assert "Maximum request body size 2097152 exceeded, actual body size" in resp_text - # Maximum request body size X exceeded, actual body size X - body_size = int(resp_text.split()[-1]) - assert body_size >= custom_max_size + assert "Maximum request body size 2097152 exceeded" in resp_text await resp.release() @@ -1781,11 +1796,10 @@ async def handler(request): assert 413 == resp.status resp_text = await resp.text() - assert ( - "Maximum request body size 10 exceeded, " - "actual body size 1024" in resp_text - ) - data["file"].close() + assert "Maximum request body size 10 exceeded" in resp_text + data_file = data["file"] + assert isinstance(data_file, io.BytesIO) + data_file.close() await resp.release() diff --git a/tests/test_web_protocol.py b/tests/test_web_protocol.py index 5ae1e5dd756..f823986b7b2 100644 --- a/tests/test_web_protocol.py +++ b/tests/test_web_protocol.py @@ -1,48 +1,50 @@ import asyncio -from typing import Any, cast from unittest import mock -from aiohttp.web_protocol import RequestHandler +import pytest +from aiohttp.http import WebSocketReader +from aiohttp.web_protocol import RequestHandler +from aiohttp.web_server import Server -class _DummyManager: - def __init__(self) -> None: - self.request_handler = mock.Mock() - self.request_factory = mock.Mock() +@pytest.fixture +def dummy_manager() -> Server: + return mock.create_autospec(Server, request_handler=mock.Mock(), request_factory=mock.Mock(), instance=True) # type: ignore[no-any-return] -class _DummyParser: - def __init__(self) -> None: - self.received: list[bytes] = [] - def feed_data(self, data: bytes) -> tuple[bool, bytes]: - self.received.append(data) - return False, b"" +@pytest.fixture +def dummy_reader() -> tuple[WebSocketReader, mock.Mock]: + m = mock.create_autospec(WebSocketReader, spec_set=True, instance=True) + m.feed_data.return_value = False, b"" + return m, m def test_set_parser_does_not_call_data_received_cb_for_tail( loop: asyncio.AbstractEventLoop, + dummy_manager: Server, + dummy_reader: tuple[WebSocketReader, mock.Mock], ) -> None: - handler: RequestHandler[Any] = RequestHandler(cast(Any, _DummyManager()), loop=loop) + handler = RequestHandler(dummy_manager, loop=loop) handler._message_tail = b"tail" cb = mock.Mock() - parser = _DummyParser() - handler.set_parser(parser, data_received_cb=cb) + handler.set_parser(dummy_reader[0], data_received_cb=cb) cb.assert_not_called() - assert parser.received == [b"tail"] + dummy_reader[1].feed_data.assert_called_once_with(b"tail") def test_data_received_calls_data_received_cb( loop: asyncio.AbstractEventLoop, + dummy_manager: Server, + dummy_reader: tuple[WebSocketReader, mock.Mock], ) -> None: - handler: RequestHandler[Any] = RequestHandler(cast(Any, _DummyManager()), loop=loop) + handler = RequestHandler(dummy_manager, loop=loop) cb = mock.Mock() - parser = _DummyParser() - handler.set_parser(parser, data_received_cb=cb) + handler.set_parser(dummy_reader[0], data_received_cb=cb) handler.data_received(b"x") - assert cb.call_count == 1 - assert parser.received == [b"x"] + cb.assert_called_once() + dummy_reader[1].feed_data.assert_called_once_with(b"x") diff --git a/tests/test_web_request.py b/tests/test_web_request.py index 027e68ca746..fdbc3456d18 100644 --- a/tests/test_web_request.py +++ b/tests/test_web_request.py @@ -876,8 +876,8 @@ def test_clone_headers_dict() -> None: assert req2.raw_headers == ((b"B", b"C"),) -async def test_cannot_clone_after_read(protocol) -> None: - payload = StreamReader(protocol, 2**16, loop=asyncio.get_event_loop()) +async def test_cannot_clone_after_read(protocol: BaseProtocol) -> None: + payload = StreamReader(protocol, 2**18, loop=asyncio.get_event_loop()) payload.feed_data(b"data") payload.feed_eof() req = make_mocked_request("GET", "/path", payload=payload) @@ -899,7 +899,18 @@ async def test_make_too_big_request(protocol) -> None: assert err.value.status_code == 413 -async def test_make_too_big_request_adjust_limit(protocol) -> None: +async def test_make_too_big_request_same_size_to_max(protocol: BaseProtocol) -> None: + payload = StreamReader(protocol, 2**16, loop=asyncio.get_event_loop()) + large_file = 1024**2 * b"x" + payload.feed_data(large_file) + payload.feed_eof() + req = make_mocked_request("POST", "/", payload=payload) + resp_text = await req.read() + + assert resp_text == large_file + + +async def test_make_too_big_request_adjust_limit(protocol: BaseProtocol) -> None: payload = StreamReader(protocol, 2**16, loop=asyncio.get_event_loop()) large_file = 1024**2 * b"x" too_large_file = large_file + b"x" @@ -937,7 +948,7 @@ async def test_multipart_formdata(protocol) -> None: async def test_multipart_formdata_field_missing_name(protocol: BaseProtocol) -> None: # Ensure ValueError is raised when Content-Disposition has no name - payload = StreamReader(protocol, 2**16, loop=asyncio.get_event_loop()) + payload = StreamReader(protocol, 2**18, loop=asyncio.get_event_loop()) payload.feed_data( b"-----------------------------326931944431359\r\n" b"Content-Disposition: form-data\r\n" # Missing name! @@ -989,7 +1000,7 @@ async def test_multipart_formdata_headers_too_many(protocol: BaseProtocol) -> No b"--b--\r\n" ) content_type = "multipart/form-data; boundary=b" - payload = StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + payload = StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) payload.feed_data(body) payload.feed_eof() req = make_mocked_request( @@ -1016,7 +1027,7 @@ async def test_multipart_formdata_header_too_long(protocol: BaseProtocol) -> Non b"--b--\r\n" ) content_type = "multipart/form-data; boundary=b" - payload = StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + payload = StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) payload.feed_data(body) payload.feed_eof() req = make_mocked_request( diff --git a/tests/test_websocket_parser.py b/tests/test_websocket_parser.py index d98ceb4b19c..01e786787f7 100644 --- a/tests/test_websocket_parser.py +++ b/tests/test_websocket_parser.py @@ -19,7 +19,7 @@ from aiohttp._websocket.reader import WebSocketDataQueue from aiohttp.base_protocol import BaseProtocol from aiohttp.compression_utils import ZLibBackend -from aiohttp.http import WebSocketError, WSCloseCode, WSMessage, WSMsgType +from aiohttp.http import HttpParser, WebSocketError, WSCloseCode, WSMessage, WSMsgType from aiohttp.http_websocket import WebSocketReader @@ -106,8 +106,9 @@ def build_close_frame(code=1000, message=b"", noheader=False): @pytest.fixture() def protocol(loop: asyncio.AbstractEventLoop) -> BaseProtocol: + parser = mock.create_autospec(HttpParser, spec_set=True, instance=True) transport = mock.Mock(spec_set=asyncio.Transport) - protocol = BaseProtocol(loop) + protocol = BaseProtocol(loop, parser=parser) protocol.connection_made(transport) return protocol diff --git a/tests/test_websocket_writer.py b/tests/test_websocket_writer.py index d0b1b972a58..272374c7187 100644 --- a/tests/test_websocket_writer.py +++ b/tests/test_websocket_writer.py @@ -152,7 +152,7 @@ async def test_send_compress_cancelled( monkeypatch.setattr("aiohttp._websocket.writer.WEBSOCKET_MAX_SYNC_CHUNK_SIZE", 1024) writer = WebSocketWriter(protocol, transport, compress=15) loop = asyncio.get_running_loop() - queue = WebSocketDataQueue(mock.Mock(_reading_paused=False), 2**16, loop=loop) + queue = WebSocketDataQueue(mock.Mock(_reading_paused=False), 2**18, loop=loop) reader = WebSocketReader(queue, 50000) # Replace executor with slow one to make race condition reproducible @@ -299,7 +299,7 @@ async def test_concurrent_messages( ): writer = WebSocketWriter(protocol, transport, compress=15) loop = asyncio.get_running_loop() - queue = WebSocketDataQueue(mock.Mock(_reading_paused=False), 2**16, loop=loop) + queue = WebSocketDataQueue(mock.Mock(_reading_paused=False), 2**18, loop=loop) reader = WebSocketReader(queue, 50000) writers = [] payloads = [] From 04ed4bd019c395a374a0e9e2eac43f53c921fa65 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 13 Apr 2026 19:16:31 +0100 Subject: [PATCH 010/191] Use DEFAULT_CHUNK_SIZE global (#12356) (#12365) (cherry picked from commit bec74bb4bdfe81cc76cd9a96f6e0b01e9c1c590a) --- aiohttp/_websocket/writer.py | 5 +- aiohttp/client.py | 5 +- aiohttp/client_proto.py | 3 +- aiohttp/compression_utils.py | 3 - aiohttp/helpers.py | 4 ++ aiohttp/http_parser.py | 6 +- aiohttp/multipart.py | 12 ++-- aiohttp/payload.py | 32 ++++++---- aiohttp/streams.py | 3 +- aiohttp/web_fileresponse.py | 4 +- aiohttp/web_protocol.py | 4 +- aiohttp/web_request.py | 3 +- aiohttp/web_urldispatcher.py | 6 +- aiohttp/web_ws.py | 14 +++-- tests/test_benchmarks_http_websocket.py | 9 +-- tests/test_client_functional.py | 8 +-- tests/test_http_parser.py | 83 ++++++++++++++++++------- tests/test_http_writer.py | 7 ++- tests/test_multipart.py | 8 ++- tests/test_payload.py | 68 ++++++++++---------- tests/test_streams.py | 15 ++--- tests/test_web_request.py | 13 ++-- tests/test_websocket_writer.py | 9 ++- 23 files changed, 194 insertions(+), 130 deletions(-) diff --git a/aiohttp/_websocket/writer.py b/aiohttp/_websocket/writer.py index 5e4bbf62f90..d293171e38b 100644 --- a/aiohttp/_websocket/writer.py +++ b/aiohttp/_websocket/writer.py @@ -9,6 +9,7 @@ from ..base_protocol import BaseProtocol from ..client_exceptions import ClientConnectionResetError from ..compression_utils import ZLibBackend, ZLibCompressor +from ..helpers import DEFAULT_CHUNK_SIZE from .helpers import ( MASK_LEN, MSG_SIZE, @@ -21,8 +22,6 @@ ) from .models import WS_DEFLATE_TRAILING, WSMsgType -DEFAULT_LIMIT: Final[int] = 2**18 - # WebSocket opcode boundary: opcodes 0-7 are data frames, 8-15 are control frames # Control frames (ping, pong, close) are never compressed WS_CONTROL_FRAME_OPCODE: Final[int] = 8 @@ -52,7 +51,7 @@ def __init__( transport: asyncio.Transport, *, use_mask: bool = False, - limit: int = DEFAULT_LIMIT, + limit: int = DEFAULT_CHUNK_SIZE, random: random.Random = random.Random(), compress: int = 0, notakeover: bool = False, diff --git a/aiohttp/client.py b/aiohttp/client.py index 890555e5783..09aa4d154de 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -91,6 +91,7 @@ from .helpers import ( _SENTINEL, DEBUG, + DEFAULT_CHUNK_SIZE, EMPTY_BODY_METHODS, BasicAuth, TimeoutHandle, @@ -333,7 +334,7 @@ def __init__( trust_env: bool = False, requote_redirect_url: bool = True, trace_configs: list[TraceConfig] | None = None, - read_bufsize: int = 2**18, + read_bufsize: int = DEFAULT_CHUNK_SIZE, max_line_size: int = 8190, max_field_size: int = 8190, max_headers: int = 128, @@ -1294,7 +1295,7 @@ async def _ws_connect( transport = conn.transport assert transport is not None - reader = WebSocketDataQueue(conn_proto, 2**18, loop=self._loop) + reader = WebSocketDataQueue(conn_proto, DEFAULT_CHUNK_SIZE, loop=self._loop) writer = WebSocketWriter( conn_proto, transport, diff --git a/aiohttp/client_proto.py b/aiohttp/client_proto.py index 011fc7217fd..e996d43eb2e 100644 --- a/aiohttp/client_proto.py +++ b/aiohttp/client_proto.py @@ -12,6 +12,7 @@ ) from .helpers import ( _EXC_SENTINEL, + DEFAULT_CHUNK_SIZE, EMPTY_BODY_STATUS_CODES, BaseTimerContext, set_exception, @@ -230,7 +231,7 @@ def set_response_params( read_until_eof: bool = False, auto_decompress: bool = True, read_timeout: float | None = None, - read_bufsize: int = 2**18, + read_bufsize: int = DEFAULT_CHUNK_SIZE, timeout_ceil_threshold: float = 5, max_line_size: int = 8190, max_field_size: int = 8190, diff --git a/aiohttp/compression_utils.py b/aiohttp/compression_utils.py index 92c44da53c8..c836b6a3da4 100644 --- a/aiohttp/compression_utils.py +++ b/aiohttp/compression_utils.py @@ -34,9 +34,6 @@ MAX_SYNC_CHUNK_SIZE = 4096 -# Matches the max size we receive from sockets: -# https://github.com/python/cpython/blob/1857a40807daeae3a1bf5efb682de9c9ae6df845/Lib/asyncio/selector_events.py#L766 -DEFAULT_MAX_DECOMPRESS_SIZE = 256 * 1024 # Unlimited decompression constants - different libraries use different conventions ZLIB_MAX_LENGTH_UNLIMITED = 0 # zlib uses 0 to mean unlimited diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index a399c1c20ef..ebc73e19205 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -58,6 +58,10 @@ PY_311 = sys.version_info >= (3, 11) +# This is the default size/limit for several operations. +# Matches the max size we receive from sockets: +# https://github.com/python/cpython/blob/1857a40807daeae3a1bf5efb682de9c9ae6df845/Lib/asyncio/selector_events.py#L766 +DEFAULT_CHUNK_SIZE = 2**18 # 256 KiB _T = TypeVar("_T") _S = TypeVar("_S") diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py index 3fb5368e2bf..dce38f1c058 100644 --- a/aiohttp/http_parser.py +++ b/aiohttp/http_parser.py @@ -22,7 +22,6 @@ from . import hdrs from .base_protocol import BaseProtocol from .compression_utils import ( - DEFAULT_MAX_DECOMPRESS_SIZE, HAS_BROTLI, HAS_ZSTD, BrotliDecompressor, @@ -32,6 +31,7 @@ from .helpers import ( _EXC_SENTINEL, DEBUG, + DEFAULT_CHUNK_SIZE, EMPTY_BODY_METHODS, EMPTY_BODY_STATUS_CODES, NO_EXTENSIONS, @@ -817,7 +817,7 @@ def __init__( max_line_size: int = 8190, max_field_size: int = 8190, max_trailers: int = 128, - limit: int = DEFAULT_MAX_DECOMPRESS_SIZE, + limit: int = DEFAULT_CHUNK_SIZE, ) -> None: self._length = 0 self._paused = False @@ -1074,7 +1074,7 @@ def __init__( self, out: StreamReader, encoding: str | None, - max_decompress_size: int = DEFAULT_MAX_DECOMPRESS_SIZE, + max_decompress_size: int = DEFAULT_CHUNK_SIZE, ) -> None: self.out = out self.size = 0 diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py index cf990ded777..5cea890d343 100644 --- a/aiohttp/multipart.py +++ b/aiohttp/multipart.py @@ -14,11 +14,7 @@ from multidict import CIMultiDict, CIMultiDictProxy from .abc import AbstractStreamWriter -from .compression_utils import ( - DEFAULT_MAX_DECOMPRESS_SIZE, - ZLibCompressor, - ZLibDecompressor, -) +from .compression_utils import ZLibCompressor, ZLibDecompressor from .hdrs import ( CONTENT_DISPOSITION, CONTENT_ENCODING, @@ -26,7 +22,7 @@ CONTENT_TRANSFER_ENCODING, CONTENT_TYPE, ) -from .helpers import CHAR, TOKEN, parse_mimetype, reify +from .helpers import CHAR, DEFAULT_CHUNK_SIZE, TOKEN, parse_mimetype, reify from .http import HeadersParser from .http_exceptions import BadHttpMessage from .log import internal_logger @@ -267,7 +263,7 @@ def __init__( *, subtype: str = "mixed", default_charset: str | None = None, - max_decompress_size: int = DEFAULT_MAX_DECOMPRESS_SIZE, + max_decompress_size: int = DEFAULT_CHUNK_SIZE, client_max_size: int = sys.maxsize, max_size_error_cls: type[Exception] = ValueError, ) -> None: @@ -640,7 +636,7 @@ async def as_bytes(self, encoding: str = "utf-8", errors: str = "strict") -> byt async def write(self, writer: AbstractStreamWriter) -> None: field = self._value - while chunk := await field.read_chunk(size=2**18): + while chunk := await field.read_chunk(size=DEFAULT_CHUNK_SIZE): async for d in field.decode_iter(chunk): await writer.write(d) diff --git a/aiohttp/payload.py b/aiohttp/payload.py index 5b4cf94800c..dfc51831dad 100644 --- a/aiohttp/payload.py +++ b/aiohttp/payload.py @@ -17,6 +17,7 @@ from .abc import AbstractStreamWriter from .helpers import ( _SENTINEL, + DEFAULT_CHUNK_SIZE, content_disposition_header, guess_filename, parse_mimetype, @@ -43,7 +44,6 @@ ) TOO_LARGE_BYTES_BODY: Final[int] = 2**20 # 1 MB -READ_SIZE: Final[int] = 2**18 # 256 KiB _CLOSE_FUTURES: set[asyncio.Future[None]] = set() @@ -491,7 +491,7 @@ def _read_and_available_len( Args: remaining_content_len: Optional limit on how many bytes to read in this operation. - If None, READ_SIZE will be used as the default chunk size. + If None, DEFAULT_CHUNK_SIZE will be used as the default chunk size. Returns: A tuple containing: @@ -506,7 +506,11 @@ def _read_and_available_len( self._set_or_restore_start_position() size = self.size # Call size only once since it does I/O return size, self._value.read( - min(READ_SIZE, size or READ_SIZE, remaining_content_len or READ_SIZE) + min( + DEFAULT_CHUNK_SIZE, + size or DEFAULT_CHUNK_SIZE, + remaining_content_len or DEFAULT_CHUNK_SIZE, + ) ) def _read(self, remaining_content_len: int | None) -> bytes: @@ -515,7 +519,7 @@ def _read(self, remaining_content_len: int | None) -> bytes: Args: remaining_content_len: Optional maximum number of bytes to read. - If None, READ_SIZE will be used as the default chunk size. + If None, DEFAULT_CHUNK_SIZE will be used as the default chunk size. Returns: A chunk of bytes read from the file object, respecting the @@ -525,7 +529,7 @@ def _read(self, remaining_content_len: int | None) -> bytes: the initial _read_and_available_len call has been made. """ - return self._value.read(remaining_content_len or READ_SIZE) # type: ignore[no-any-return] + return self._value.read(remaining_content_len or DEFAULT_CHUNK_SIZE) # type: ignore[no-any-return] @property def size(self) -> int | None: @@ -628,9 +632,9 @@ async def write_with_length( None, self._read, ( - min(READ_SIZE, remaining_content_len) + min(DEFAULT_CHUNK_SIZE, remaining_content_len) if remaining_content_len is not None - else READ_SIZE + else DEFAULT_CHUNK_SIZE ), ) @@ -756,7 +760,7 @@ def _read_and_available_len( Args: remaining_content_len: Optional limit on how many bytes to read in this operation. - If None, READ_SIZE will be used as the default chunk size. + If None, DEFAULT_CHUNK_SIZE will be used as the default chunk size. Returns: A tuple containing: @@ -775,7 +779,11 @@ def _read_and_available_len( self._set_or_restore_start_position() size = self.size chunk = self._value.read( - min(READ_SIZE, size or READ_SIZE, remaining_content_len or READ_SIZE) + min( + DEFAULT_CHUNK_SIZE, + size or DEFAULT_CHUNK_SIZE, + remaining_content_len or DEFAULT_CHUNK_SIZE, + ) ) return size, chunk.encode(self._encoding) if self._encoding else chunk.encode() @@ -785,7 +793,7 @@ def _read(self, remaining_content_len: int | None) -> bytes: Args: remaining_content_len: Optional maximum number of bytes to read. - If None, READ_SIZE will be used as the default chunk size. + If None, DEFAULT_CHUNK_SIZE will be used as the default chunk size. Returns: A chunk of bytes read from the file object and encoded using the payload's @@ -797,7 +805,7 @@ def _read(self, remaining_content_len: int | None) -> bytes: the specified encoding (or UTF-8 if none was provided). """ - chunk = self._value.read(remaining_content_len or READ_SIZE) + chunk = self._value.read(remaining_content_len or DEFAULT_CHUNK_SIZE) return chunk.encode(self._encoding) if self._encoding else chunk.encode() def decode(self, encoding: str = "utf-8", errors: str = "strict") -> str: @@ -881,7 +889,7 @@ async def write_with_length( self._set_or_restore_start_position() loop_count = 0 remaining_bytes = content_length - while chunk := self._value.read(READ_SIZE): + while chunk := self._value.read(DEFAULT_CHUNK_SIZE): if loop_count > 0: # Avoid blocking the event loop # if they pass a large BytesIO object diff --git a/aiohttp/streams.py b/aiohttp/streams.py index bf26b59d49e..42b427ee0f0 100644 --- a/aiohttp/streams.py +++ b/aiohttp/streams.py @@ -7,6 +7,7 @@ from .base_protocol import BaseProtocol from .helpers import ( _EXC_SENTINEL, + DEFAULT_CHUNK_SIZE, BaseTimerContext, TimerNoop, set_exception, @@ -167,7 +168,7 @@ def __repr__(self) -> str: info.append("%d bytes" % self._size) if self._eof: info.append("eof") - if self._low_water != 2**18: # default limit + if self._low_water != DEFAULT_CHUNK_SIZE: info.append("low=%d high=%d" % (self._low_water, self._high_water)) if self._waiter: info.append("w=%r" % self._waiter) diff --git a/aiohttp/web_fileresponse.py b/aiohttp/web_fileresponse.py index d0a6972fdea..8aa67cec7c1 100644 --- a/aiohttp/web_fileresponse.py +++ b/aiohttp/web_fileresponse.py @@ -26,7 +26,7 @@ from . import hdrs from .abc import AbstractStreamWriter -from .helpers import ETAG_ANY, ETag, must_be_empty_body +from .helpers import DEFAULT_CHUNK_SIZE, ETAG_ANY, ETag, must_be_empty_body from .typedefs import LooseHeaders, PathLike from .web_exceptions import ( HTTPForbidden, @@ -95,7 +95,7 @@ class FileResponse(StreamResponse): def __init__( self, path: PathLike, - chunk_size: int = 256 * 1024, + chunk_size: int = DEFAULT_CHUNK_SIZE, status: int = 200, reason: str | None = None, headers: LooseHeaders | None = None, diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py index be63b824b45..f170c8e5de6 100644 --- a/aiohttp/web_protocol.py +++ b/aiohttp/web_protocol.py @@ -17,7 +17,7 @@ from .abc import AbstractAccessLogger, AbstractStreamWriter from .base_protocol import BaseProtocol -from .helpers import ceil_timeout +from .helpers import DEFAULT_CHUNK_SIZE, ceil_timeout from .http import ( HttpProcessingError, HttpRequestParser, @@ -182,7 +182,7 @@ def __init__( max_headers: int = 128, max_field_size: int = 8190, lingering_time: float = 10.0, - read_bufsize: int = 2**18, + read_bufsize: int = DEFAULT_CHUNK_SIZE, auto_decompress: bool = True, timeout_ceil_threshold: float = 5, ): diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py index 826a88337a0..47d50a12d5c 100644 --- a/aiohttp/web_request.py +++ b/aiohttp/web_request.py @@ -29,6 +29,7 @@ from .helpers import ( _SENTINEL, DEBUG, + DEFAULT_CHUNK_SIZE, ETAG_ANY, LIST_QUOTED_ETAG_RE, ChainMapProxy, @@ -752,7 +753,7 @@ async def post(self) -> "MultiDictProxy[str | bytes | FileField]": tmp = await self._loop.run_in_executor( None, tempfile.TemporaryFile ) - while chunk := await field.read_chunk(size=2**18): + while chunk := await field.read_chunk(size=DEFAULT_CHUNK_SIZE): async for decoded_chunk in field.decode_iter(chunk): await self._loop.run_in_executor( None, tmp.write, decoded_chunk diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index a725f7d792c..74351eecb06 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -31,7 +31,7 @@ from . import hdrs from .abc import AbstractMatchInfo, AbstractRouter, AbstractView -from .helpers import DEBUG +from .helpers import DEBUG, DEFAULT_CHUNK_SIZE from .http import HttpVersion11 from .typedefs import Handler, PathLike from .web_exceptions import ( @@ -536,7 +536,7 @@ def __init__( *, name: str | None = None, expect_handler: _ExpectHandler | None = None, - chunk_size: int = 256 * 1024, + chunk_size: int = DEFAULT_CHUNK_SIZE, show_index: bool = False, follow_symlinks: bool = False, append_version: bool = False, @@ -1166,7 +1166,7 @@ def add_static( *, name: str | None = None, expect_handler: _ExpectHandler | None = None, - chunk_size: int = 256 * 1024, + chunk_size: int = DEFAULT_CHUNK_SIZE, show_index: bool = False, follow_symlinks: bool = False, append_version: bool = False, diff --git a/aiohttp/web_ws.py b/aiohttp/web_ws.py index 74811fdd283..5aa75b715a0 100644 --- a/aiohttp/web_ws.py +++ b/aiohttp/web_ws.py @@ -12,10 +12,14 @@ from . import hdrs from ._websocket.reader import WebSocketDataQueue -from ._websocket.writer import DEFAULT_LIMIT from .abc import AbstractStreamWriter from .client_exceptions import WSMessageTypeError -from .helpers import calculate_timeout_when, set_exception, set_result +from .helpers import ( + DEFAULT_CHUNK_SIZE, + calculate_timeout_when, + set_exception, + set_result, +) from .http import ( WS_CLOSED_MESSAGE, WS_CLOSING_MESSAGE, @@ -104,7 +108,7 @@ def __init__( protocols: Iterable[str] = (), compress: bool = True, max_msg_size: int = 4 * 1024 * 1024, - writer_limit: int = DEFAULT_LIMIT, + writer_limit: int = DEFAULT_CHUNK_SIZE, decode_text: bool = True, ) -> None: super().__init__(status=101) @@ -380,7 +384,9 @@ def _post_start( loop = self._loop assert loop is not None - self._reader = WebSocketDataQueue(request._protocol, 2**18, loop=loop) + self._reader = WebSocketDataQueue( + request._protocol, DEFAULT_CHUNK_SIZE, loop=loop + ) parser = WebSocketReader( self._reader, self._max_msg_size, diff --git a/tests/test_benchmarks_http_websocket.py b/tests/test_benchmarks_http_websocket.py index f40db171919..61b23125460 100644 --- a/tests/test_benchmarks_http_websocket.py +++ b/tests/test_benchmarks_http_websocket.py @@ -8,6 +8,7 @@ from aiohttp._websocket.helpers import MSG_SIZE, PACK_LEN3 from aiohttp._websocket.reader import WebSocketDataQueue from aiohttp.base_protocol import BaseProtocol +from aiohttp.helpers import DEFAULT_CHUNK_SIZE from aiohttp.http_websocket import WebSocketReader, WebSocketWriter, WSMsgType @@ -15,8 +16,8 @@ def test_read_large_binary_websocket_messages( loop: asyncio.AbstractEventLoop, benchmark: BenchmarkFixture ) -> None: """Read one hundred large binary websocket messages.""" - queue = WebSocketDataQueue(BaseProtocol(loop), 2**16, loop=loop) - reader = WebSocketReader(queue, max_msg_size=2**18) + queue = WebSocketDataQueue(BaseProtocol(loop), DEFAULT_CHUNK_SIZE, loop=loop) + reader = WebSocketReader(queue, max_msg_size=DEFAULT_CHUNK_SIZE) # PACK3 has a minimum message length of 2**16 bytes. message = b"x" * ((2**16) + 1) @@ -36,8 +37,8 @@ def test_read_one_hundred_websocket_text_messages( loop: asyncio.AbstractEventLoop, benchmark: BenchmarkFixture ) -> None: """Benchmark reading 100 WebSocket text messages.""" - queue = WebSocketDataQueue(BaseProtocol(loop), 2**18, loop=loop) - reader = WebSocketReader(queue, max_msg_size=2**18) + queue = WebSocketDataQueue(BaseProtocol(loop), DEFAULT_CHUNK_SIZE, loop=loop) + reader = WebSocketReader(queue, max_msg_size=DEFAULT_CHUNK_SIZE) raw_message = ( b'\x81~\x01!{"id":1,"src":"shellyplugus-c049ef8c30e4","dst":"aios-1453812500' b'8","result":{"name":null,"id":"shellyplugus-c049ef8c30e4","mac":"C049EF8C30E' diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 05e6574b89f..6d17172457b 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -51,8 +51,8 @@ TooManyRedirects, ) from aiohttp.client_reqrep import ClientRequest -from aiohttp.compression_utils import DEFAULT_MAX_DECOMPRESS_SIZE from aiohttp.connector import Connection +from aiohttp.helpers import DEFAULT_CHUNK_SIZE from aiohttp.http_writer import StreamWriter from aiohttp.payload import ( AsyncIterablePayload, @@ -2457,7 +2457,7 @@ async def test_payload_decompress_size_limit(aiohttp_client: AiohttpClient) -> N payload_size = 64 * 2**20 original = b"A" * payload_size compressed = zlib.compress(original) - assert len(original) > DEFAULT_MAX_DECOMPRESS_SIZE + assert len(original) > DEFAULT_CHUNK_SIZE async def handler(request: web.Request) -> web.Response: # Send compressed data with Content-Encoding header @@ -2489,7 +2489,7 @@ async def test_payload_decompress_size_limit_brotli( payload_size = 64 * 2**20 original = b"A" * payload_size compressed = brotli.compress(original) - assert len(original) > DEFAULT_MAX_DECOMPRESS_SIZE + assert len(original) > DEFAULT_CHUNK_SIZE async def handler(request: web.Request) -> web.Response: resp = web.Response(body=compressed) @@ -2521,7 +2521,7 @@ async def test_payload_decompress_size_limit_zstd( original = b"A" * payload_size compressor = ZstdCompressor() compressed = compressor.compress(original) + compressor.flush() - assert len(original) > DEFAULT_MAX_DECOMPRESS_SIZE + assert len(original) > DEFAULT_CHUNK_SIZE async def handler(request: web.Request) -> web.Response: resp = web.Response(body=compressed) diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index 6f9caa2fe55..9c65fb95257 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -19,6 +19,7 @@ from aiohttp import http_exceptions, streams from aiohttp.base_protocol import BaseProtocol from aiohttp.client_proto import ResponseHandler +from aiohttp.helpers import DEFAULT_CHUNK_SIZE from aiohttp.http_parser import ( NO_EXTENSIONS, DeflateBuffer, @@ -100,7 +101,7 @@ def parser( parser = request.param( protocol, loop, - 2**18, + DEFAULT_CHUNK_SIZE, max_line_size=8190, max_headers=128, max_field_size=8190, @@ -128,7 +129,7 @@ def response( parser = request.param( protocol, loop, - 2**18, + DEFAULT_CHUNK_SIZE, max_line_size=8190, max_headers=128, max_field_size=8190, @@ -1673,7 +1674,7 @@ async def test_http_response_parser_bad_chunked_strict_py( response = HttpResponseParserPy( protocol, loop, - 2**18, + DEFAULT_CHUNK_SIZE, max_line_size=8190, max_field_size=8190, ) @@ -1849,7 +1850,7 @@ def test_parse_no_length_or_te_on_post( request_cls: type[HttpRequestParser], ) -> None: protocol = RequestHandler(server, loop=loop) - parser = request_cls(protocol, loop, limit=2**18) + parser = request_cls(protocol, loop, limit=DEFAULT_CHUNK_SIZE) protocol._parser = parser text = b"POST /test HTTP/1.1\r\n\r\n" msg, payload = parser.feed_data(text)[0][0] @@ -2128,7 +2129,7 @@ def test_parse_bad_method_for_c_parser_raises( parser = HttpRequestParserC( protocol, loop, - 2**18, + DEFAULT_CHUNK_SIZE, max_line_size=8190, max_headers=128, max_field_size=8190, @@ -2150,7 +2151,9 @@ async def test_parse_eof_payload(self, protocol: BaseProtocol) -> None: assert [(bytearray(b"data"))] == list(out._buffer) async def test_parse_length_payload_eof(self, protocol: BaseProtocol) -> None: - out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) p = HttpPayloadParser(out, length=4, headers_parser=HeadersParser()) p.feed_data(b"da") @@ -2174,7 +2177,9 @@ async def test_parse_chunked_payload_size_data_mismatch( Regression test for #10596. """ - out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser()) # Declared chunk-size is 4 but actual data is "Hello" (5 bytes). # After consuming 4 bytes, remaining starts with "o" not "\r\n". @@ -2189,7 +2194,9 @@ async def test_parse_chunked_payload_size_data_mismatch_too_short( Regression test for #10596. """ - out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser()) # Declared chunk-size is 6 but actual data before CRLF is "Hello" (5 bytes). # Parser reads 6 bytes: "Hello\r", then expects \r\n but sees "\n0\r\n..." @@ -2211,7 +2218,9 @@ async def test_parse_chunked_payload_split_end( async def test_parse_chunked_payload_split_end2( self, protocol: BaseProtocol ) -> None: - out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser()) p.feed_data(b"4\r\nasdf\r\n0\r\n\r") p.feed_data(b"\n") @@ -2222,7 +2231,9 @@ async def test_parse_chunked_payload_split_end2( async def test_parse_chunked_payload_split_end_trailers( self, protocol: BaseProtocol ) -> None: - out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser()) p.feed_data(b"4\r\nasdf\r\n0\r\n") p.feed_data(b"Content-MD5: 912ec803b2ce49e4a541068d495ab570\r\n") @@ -2234,7 +2245,9 @@ async def test_parse_chunked_payload_split_end_trailers( async def test_parse_chunked_payload_split_end_trailers2( self, protocol: BaseProtocol ) -> None: - out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser()) p.feed_data(b"4\r\nasdf\r\n0\r\n") p.feed_data(b"Content-MD5: 912ec803b2ce49e4a541068d495ab570\r\n\r") @@ -2266,7 +2279,9 @@ async def test_parse_chunked_payload_split_end_trailers4( assert b"asdf" == b"".join(out._buffer) async def test_http_payload_parser_length(self, protocol: BaseProtocol) -> None: - out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) p = HttpPayloadParser(out, length=2, headers_parser=HeadersParser()) state, tail = p.feed_data(b"1245") assert state is PayloadState.PAYLOAD_COMPLETE @@ -2279,7 +2294,9 @@ async def test_http_payload_parser_deflate(self, protocol: BaseProtocol) -> None COMPRESSED = b"x\x9cKI,I\x04\x00\x04\x00\x01\x9b" length = len(COMPRESSED) - out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) p = HttpPayloadParser( out, length=length, compression="deflate", headers_parser=HeadersParser() ) @@ -2350,7 +2367,9 @@ async def test_http_payload_parser_deflate_split_err( async def test_http_payload_parser_length_zero( self, protocol: BaseProtocol ) -> None: - out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) p = HttpPayloadParser(out, length=0, headers_parser=HeadersParser()) assert p.done assert out.is_eof() @@ -2358,7 +2377,9 @@ async def test_http_payload_parser_length_zero( @pytest.mark.skipif(brotli is None, reason="brotli is not installed") async def test_http_payload_brotli(self, protocol: BaseProtocol) -> None: compressed = brotli.compress(b"brotli data") - out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) p = HttpPayloadParser( out, length=len(compressed), @@ -2372,7 +2393,9 @@ async def test_http_payload_brotli(self, protocol: BaseProtocol) -> None: @pytest.mark.skipif(zstandard is None, reason="zstandard is not installed") async def test_http_payload_zstandard(self, protocol: BaseProtocol) -> None: compressed = zstandard.compress(b"zstd data") - out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) p = HttpPayloadParser( out, length=len(compressed), @@ -2390,7 +2413,9 @@ async def test_http_payload_zstandard_multi_frame( frame1 = zstandard.compress(b"first") frame2 = zstandard.compress(b"second") payload = frame1 + frame2 - out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) p = HttpPayloadParser( out, length=len(payload), @@ -2407,7 +2432,9 @@ async def test_http_payload_zstandard_multi_frame_chunked( ) -> None: frame1 = zstandard.compress(b"chunk1") frame2 = zstandard.compress(b"chunk2") - out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) p = HttpPayloadParser( out, length=len(frame1) + len(frame2), @@ -2427,7 +2454,9 @@ async def test_http_payload_zstandard_frame_split_mid_chunk( frame2 = zstandard.compress(b"BBBB") combined = frame1 + frame2 split_point = len(frame1) + 3 # 3 bytes into frame2 - out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) p = HttpPayloadParser( out, length=len(combined), @@ -2445,7 +2474,9 @@ async def test_http_payload_zstandard_many_small_frames( ) -> None: parts = [f"part{i}".encode() for i in range(10)] payload = b"".join(zstandard.compress(p) for p in parts) - out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) + out = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) p = HttpPayloadParser( out, length=len(payload), @@ -2459,7 +2490,9 @@ async def test_http_payload_zstandard_many_small_frames( class TestDeflateBuffer: async def test_feed_data(self, protocol: BaseProtocol) -> None: - buf = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) + buf = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) dbuf = DeflateBuffer(buf, "deflate") dbuf.decompressor = mock.Mock() @@ -2544,7 +2577,9 @@ async def test_feed_eof_no_err_zstandard(self, protocol: BaseProtocol) -> None: assert buf._eof async def test_empty_body(self, protocol: BaseProtocol) -> None: - buf = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) + buf = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) dbuf = DeflateBuffer(buf, "deflate") dbuf.feed_eof() @@ -2569,7 +2604,9 @@ async def test_streaming_decompress_large_payload( original = b"A" * (3 * 2**20) compressed = zlib.compress(original) - buf = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) + buf = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) dbuf = DeflateBuffer(buf, "deflate") # Feed compressed data in chunks (simulating network streaming) diff --git a/tests/test_http_writer.py b/tests/test_http_writer.py index 251645f2398..3c667bb7344 100644 --- a/tests/test_http_writer.py +++ b/tests/test_http_writer.py @@ -11,6 +11,7 @@ from aiohttp import ClientConnectionResetError, hdrs, http from aiohttp.base_protocol import BaseProtocol from aiohttp.compression_utils import ZLibBackend +from aiohttp.helpers import DEFAULT_CHUNK_SIZE from aiohttp.http_writer import _serialize_headers @@ -1423,7 +1424,7 @@ async def test_write_drain_condition_with_large_buffer( protocol._drain_helper.reset_mock() # type: ignore[attr-defined] # Write large amount of data with drain=True - large_data = b"x" * (2**18 + 1) # Just over LIMIT + large_data = b"x" * (DEFAULT_CHUNK_SIZE + 1) # Just over LIMIT await msg.write(large_data, drain=True) # Drain should be called because drain=True AND buffer_size > LIMIT @@ -1452,12 +1453,12 @@ async def test_write_no_drain_with_large_buffer( protocol._drain_helper.reset_mock() # type: ignore[attr-defined] # Write large amount of data with drain=False - large_data = b"x" * (2**18 + 1) # Just over LIMIT + large_data = b"x" * (DEFAULT_CHUNK_SIZE + 1) # Just over LIMIT await msg.write(large_data, drain=False) # Drain should NOT be called because drain=False assert not protocol._drain_helper.called # type: ignore[attr-defined] - assert msg.buffer_size == (2**18 + 1) # Buffer not reset + assert msg.buffer_size == (DEFAULT_CHUNK_SIZE + 1) # Buffer not reset assert large_data in buf diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 0d01bf6a5ae..1f80f9c2a21 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -19,7 +19,7 @@ CONTENT_TRANSFER_ENCODING, CONTENT_TYPE, ) -from aiohttp.helpers import parse_mimetype +from aiohttp.helpers import DEFAULT_CHUNK_SIZE, parse_mimetype from aiohttp.multipart import ( BodyPartReader, BodyPartReaderPayload, @@ -739,9 +739,11 @@ async def test_filename(self) -> None: assert "foo.html" == part.filename async def test_reading_long_part(self) -> None: - size = 2 * 2**18 + size = 2 * DEFAULT_CHUNK_SIZE protocol = mock.Mock(_reading_paused=False) - stream = StreamReader(protocol, 2**18, loop=asyncio.get_event_loop()) + stream = StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_event_loop() + ) stream.feed_data(b"0" * size + b"\r\n--:--") stream.feed_eof() obj = aiohttp.BodyPartReader(BOUNDARY, {}, stream) diff --git a/tests/test_payload.py b/tests/test_payload.py index f5454ec46f1..3284c213d55 100644 --- a/tests/test_payload.py +++ b/tests/test_payload.py @@ -13,7 +13,7 @@ from aiohttp import payload from aiohttp.abc import AbstractStreamWriter -from aiohttp.payload import READ_SIZE +from aiohttp.helpers import DEFAULT_CHUNK_SIZE class BufferWriter(AbstractStreamWriter): @@ -331,14 +331,13 @@ def mock_read(size: int | None = None) -> bytes: async def test_bytesio_payload_large_data_multiple_chunks() -> None: """Test BytesIOPayload with large data requiring multiple read chunks.""" - chunk_size = 2**18 # 256KiB (READ_SIZE) - data = b"x" * (chunk_size + 1000) # Slightly larger than READ_SIZE + data = b"x" * (DEFAULT_CHUNK_SIZE + 1000) payload_bytesio = payload.BytesIOPayload(io.BytesIO(data)) writer = MockStreamWriter() await payload_bytesio.write_with_length(writer, None) assert writer.get_written_bytes() == data - assert len(writer.get_written_bytes()) == chunk_size + 1000 + assert len(writer.get_written_bytes()) == DEFAULT_CHUNK_SIZE + 1000 async def test_bytesio_payload_remaining_bytes_exhausted() -> None: @@ -355,21 +354,20 @@ async def test_bytesio_payload_remaining_bytes_exhausted() -> None: async def test_iobase_payload_exact_chunk_size_limit() -> None: """Test IOBasePayload with content length matching exactly one read chunk.""" - chunk_size = 2**18 # 256KiB (READ_SIZE) - data = b"x" * chunk_size + b"extra" # Slightly larger than one read chunk + data = b"x" * DEFAULT_CHUNK_SIZE + b"extra" # Slightly larger than one read chunk p = payload.IOBasePayload(io.BytesIO(data)) writer = MockStreamWriter() - await p.write_with_length(writer, chunk_size) + await p.write_with_length(writer, DEFAULT_CHUNK_SIZE) written = writer.get_written_bytes() - assert len(written) == chunk_size - assert written == data[:chunk_size] + assert len(written) == DEFAULT_CHUNK_SIZE + assert written == data[:DEFAULT_CHUNK_SIZE] async def test_iobase_payload_reads_in_chunks() -> None: - """Test IOBasePayload reads data in chunks of READ_SIZE, not all at once.""" - # Create a large file that's multiple times larger than READ_SIZE - large_data = b"x" * (READ_SIZE * 3 + 1000) # ~192KB + 1000 bytes + """Test IOBasePayload reads data in chunks of default size, not all at once.""" + # Create a large file that's multiple times larger than DEFAULT_CHUNK_SIZE + large_data = b"x" * (DEFAULT_CHUNK_SIZE * 3 + 1000) # ~192KB + 1000 bytes # Mock the file-like object to track read calls mock_file = unittest.mock.Mock(spec=io.BytesIO) @@ -386,11 +384,11 @@ def mock_read(size: int) -> bytes: if call_count == 1: return large_data[:size] elif call_count == 2: - return large_data[READ_SIZE : READ_SIZE + size] + return large_data[DEFAULT_CHUNK_SIZE : DEFAULT_CHUNK_SIZE + size] elif call_count == 3: - return large_data[READ_SIZE * 2 : READ_SIZE * 2 + size] + return large_data[DEFAULT_CHUNK_SIZE * 2 : DEFAULT_CHUNK_SIZE * 2 + size] else: - return large_data[READ_SIZE * 3 :] + return large_data[DEFAULT_CHUNK_SIZE * 3 :] mock_file.read.side_effect = mock_read @@ -400,17 +398,17 @@ def mock_read(size: int) -> bytes: # Write with a large content_length await payload_obj.write_with_length(writer, len(large_data)) - # Verify that reads were limited to READ_SIZE + # Verify that reads were limited to DEFAULT_CHUNK_SIZE assert len(read_sizes) > 1 # Should have multiple reads for read_size in read_sizes: assert ( - read_size <= READ_SIZE - ), f"Read size {read_size} exceeds READ_SIZE {READ_SIZE}" + read_size <= DEFAULT_CHUNK_SIZE + ), f"Read size {read_size} exceeds DEFAULT_CHUNK_SIZE {DEFAULT_CHUNK_SIZE}" async def test_iobase_payload_large_content_length() -> None: """Test IOBasePayload with very large content_length doesn't read all at once.""" - data = b"x" * (READ_SIZE + 1000) + data = b"x" * (DEFAULT_CHUNK_SIZE + 1000) # Create a custom file-like object that tracks read sizes class TrackingBytesIO(io.BytesIO): @@ -430,20 +428,20 @@ def read(self, size: int | None = -1) -> bytes: large_content_length = 10 * 1024 * 1024 # 10MB await payload_obj.write_with_length(writer, large_content_length) - # Verify no single read exceeded READ_SIZE + # Verify no single read exceeded DEFAULT_CHUNK_SIZE for read_size in tracking_file.read_sizes: assert ( - read_size <= READ_SIZE - ), f"Read size {read_size} exceeds READ_SIZE {READ_SIZE}" + read_size <= DEFAULT_CHUNK_SIZE + ), f"Read size {read_size} exceeds DEFAULT_CHUNK_SIZE {DEFAULT_CHUNK_SIZE}" # Verify the correct amount of data was written assert writer.get_written_bytes() == data async def test_textio_payload_reads_in_chunks() -> None: - """Test TextIOPayload reads data in chunks of READ_SIZE, not all at once.""" - # Create a large text file that's multiple times larger than READ_SIZE - large_text = "x" * (READ_SIZE * 3 + 1000) # ~192KB + 1000 chars + """Test TextIOPayload reads data in chunks of default size, not all at once.""" + # Create a large text file that's multiple times larger than DEFAULT_CHUNK_SIZE + large_text = "x" * (DEFAULT_CHUNK_SIZE * 3 + 1000) # ~192KB + 1000 chars # Mock the file-like object to track read calls mock_file = unittest.mock.Mock(spec=io.StringIO) @@ -461,11 +459,11 @@ def mock_read(size: int) -> str: if call_count == 1: return large_text[:size] elif call_count == 2: - return large_text[READ_SIZE : READ_SIZE + size] + return large_text[DEFAULT_CHUNK_SIZE : DEFAULT_CHUNK_SIZE + size] elif call_count == 3: - return large_text[READ_SIZE * 2 : READ_SIZE * 2 + size] + return large_text[DEFAULT_CHUNK_SIZE * 2 : DEFAULT_CHUNK_SIZE * 2 + size] else: - return large_text[READ_SIZE * 3 :] + return large_text[DEFAULT_CHUNK_SIZE * 3 :] mock_file.read.side_effect = mock_read @@ -475,17 +473,17 @@ def mock_read(size: int) -> str: # Write with a large content_length await payload_obj.write_with_length(writer, len(large_text.encode("utf-8"))) - # Verify that reads were limited to READ_SIZE + # Verify that reads were limited to DEFAULT_CHUNK_SIZE assert len(read_sizes) > 1 # Should have multiple reads for read_size in read_sizes: assert ( - read_size <= READ_SIZE - ), f"Read size {read_size} exceeds READ_SIZE {READ_SIZE}" + read_size <= DEFAULT_CHUNK_SIZE + ), f"Read size {read_size} exceeds DEFAULT_CHUNK_SIZE {DEFAULT_CHUNK_SIZE}" async def test_textio_payload_large_content_length() -> None: """Test TextIOPayload with very large content_length doesn't read all at once.""" - text_data = "x" * (READ_SIZE + 1000) + text_data = "x" * (DEFAULT_CHUNK_SIZE + 1000) # Create a custom file-like object that tracks read sizes class TrackingStringIO(io.StringIO): @@ -505,11 +503,11 @@ def read(self, size: int | None = -1) -> str: large_content_length = 10 * 1024 * 1024 # 10MB await payload_obj.write_with_length(writer, large_content_length) - # Verify no single read exceeded READ_SIZE + # Verify no single read exceeded DEFAULT_CHUNK_SIZE for read_size in tracking_file.read_sizes: assert ( - read_size <= READ_SIZE - ), f"Read size {read_size} exceeds READ_SIZE {READ_SIZE}" + read_size <= DEFAULT_CHUNK_SIZE + ), f"Read size {read_size} exceeds DEFAULT_CHUNK_SIZE {DEFAULT_CHUNK_SIZE}" # Verify the correct amount of data was written assert writer.get_written_bytes() == text_data.encode("utf-8") diff --git a/tests/test_streams.py b/tests/test_streams.py index a16b991e0ce..aa50b5ca2d3 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -12,6 +12,7 @@ from re_assert import Matches from aiohttp import streams +from aiohttp.helpers import DEFAULT_CHUNK_SIZE from aiohttp.http_exceptions import LineTooLong DATA = b"line1\nline2\nline3\n" @@ -25,7 +26,7 @@ def chunkify(seq, n): async def create_stream(): loop = asyncio.get_event_loop() protocol = mock.Mock(_reading_paused=False) - stream = streams.StreamReader(protocol, 2**18, loop=loop) + stream = streams.StreamReader(protocol, DEFAULT_CHUNK_SIZE, loop=loop) stream.feed_data(DATA) stream.feed_eof() return stream @@ -1293,7 +1294,7 @@ async def set_err(): async def test_feed_data_waiters(protocol) -> None: loop = asyncio.get_event_loop() - reader = streams.StreamReader(protocol, 2**18, loop=loop) + reader = streams.StreamReader(protocol, DEFAULT_CHUNK_SIZE, loop=loop) waiter = reader._waiter = loop.create_future() eof_waiter = reader._eof_waiter = loop.create_future() @@ -1321,7 +1322,7 @@ async def test_feed_data_completed_waiters(protocol) -> None: async def test_feed_eof_waiters(protocol) -> None: loop = asyncio.get_event_loop() - reader = streams.StreamReader(protocol, 2**18, loop=loop) + reader = streams.StreamReader(protocol, DEFAULT_CHUNK_SIZE, loop=loop) waiter = reader._waiter = loop.create_future() eof_waiter = reader._eof_waiter = loop.create_future() @@ -1353,7 +1354,7 @@ async def test_feed_eof_cancelled(protocol) -> None: async def test_on_eof(protocol) -> None: loop = asyncio.get_event_loop() - reader = streams.StreamReader(protocol, 2**18, loop=loop) + reader = streams.StreamReader(protocol, DEFAULT_CHUNK_SIZE, loop=loop) on_eof = mock.Mock() reader.on_eof(on_eof) @@ -1374,7 +1375,7 @@ async def test_on_eof_empty_reader() -> None: async def test_on_eof_exc_in_callback(protocol) -> None: loop = asyncio.get_event_loop() - reader = streams.StreamReader(protocol, 2**18, loop=loop) + reader = streams.StreamReader(protocol, DEFAULT_CHUNK_SIZE, loop=loop) on_eof = mock.Mock() on_eof.side_effect = ValueError @@ -1409,7 +1410,7 @@ async def test_on_eof_eof_is_set(protocol) -> None: async def test_on_eof_eof_is_set_exception(protocol) -> None: loop = asyncio.get_event_loop() - reader = streams.StreamReader(protocol, 2**18, loop=loop) + reader = streams.StreamReader(protocol, DEFAULT_CHUNK_SIZE, loop=loop) reader.feed_eof() on_eof = mock.Mock() @@ -1455,7 +1456,7 @@ async def test_set_exception_cancelled(protocol) -> None: async def test_set_exception_eof_callbacks(protocol) -> None: loop = asyncio.get_event_loop() - reader = streams.StreamReader(protocol, 2**18, loop=loop) + reader = streams.StreamReader(protocol, DEFAULT_CHUNK_SIZE, loop=loop) on_eof = mock.Mock() reader.on_eof(on_eof) diff --git a/tests/test_web_request.py b/tests/test_web_request.py index fdbc3456d18..fa2f257c402 100644 --- a/tests/test_web_request.py +++ b/tests/test_web_request.py @@ -14,6 +14,7 @@ from aiohttp import HttpVersion from aiohttp.base_protocol import BaseProtocol +from aiohttp.helpers import DEFAULT_CHUNK_SIZE from aiohttp.http_exceptions import BadHttpMessage, LineTooLong from aiohttp.http_parser import RawRequestMessage from aiohttp.streams import StreamReader @@ -877,7 +878,7 @@ def test_clone_headers_dict() -> None: async def test_cannot_clone_after_read(protocol: BaseProtocol) -> None: - payload = StreamReader(protocol, 2**18, loop=asyncio.get_event_loop()) + payload = StreamReader(protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_event_loop()) payload.feed_data(b"data") payload.feed_eof() req = make_mocked_request("GET", "/path", payload=payload) @@ -948,7 +949,7 @@ async def test_multipart_formdata(protocol) -> None: async def test_multipart_formdata_field_missing_name(protocol: BaseProtocol) -> None: # Ensure ValueError is raised when Content-Disposition has no name - payload = StreamReader(protocol, 2**18, loop=asyncio.get_event_loop()) + payload = StreamReader(protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_event_loop()) payload.feed_data( b"-----------------------------326931944431359\r\n" b"Content-Disposition: form-data\r\n" # Missing name! @@ -1000,7 +1001,9 @@ async def test_multipart_formdata_headers_too_many(protocol: BaseProtocol) -> No b"--b--\r\n" ) content_type = "multipart/form-data; boundary=b" - payload = StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) + payload = StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) payload.feed_data(body) payload.feed_eof() req = make_mocked_request( @@ -1027,7 +1030,9 @@ async def test_multipart_formdata_header_too_long(protocol: BaseProtocol) -> Non b"--b--\r\n" ) content_type = "multipart/form-data; boundary=b" - payload = StreamReader(protocol, 2**18, loop=asyncio.get_running_loop()) + payload = StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) payload.feed_data(body) payload.feed_eof() req = make_mocked_request( diff --git a/tests/test_websocket_writer.py b/tests/test_websocket_writer.py index 272374c7187..646f199d720 100644 --- a/tests/test_websocket_writer.py +++ b/tests/test_websocket_writer.py @@ -12,6 +12,7 @@ from aiohttp._websocket.reader import WebSocketDataQueue from aiohttp.base_protocol import BaseProtocol from aiohttp.compression_utils import ZLibBackend +from aiohttp.helpers import DEFAULT_CHUNK_SIZE from aiohttp.http import WebSocketReader, WebSocketWriter @@ -152,7 +153,9 @@ async def test_send_compress_cancelled( monkeypatch.setattr("aiohttp._websocket.writer.WEBSOCKET_MAX_SYNC_CHUNK_SIZE", 1024) writer = WebSocketWriter(protocol, transport, compress=15) loop = asyncio.get_running_loop() - queue = WebSocketDataQueue(mock.Mock(_reading_paused=False), 2**18, loop=loop) + queue = WebSocketDataQueue( + mock.Mock(_reading_paused=False), DEFAULT_CHUNK_SIZE, loop=loop + ) reader = WebSocketReader(queue, 50000) # Replace executor with slow one to make race condition reproducible @@ -299,7 +302,9 @@ async def test_concurrent_messages( ): writer = WebSocketWriter(protocol, transport, compress=15) loop = asyncio.get_running_loop() - queue = WebSocketDataQueue(mock.Mock(_reading_paused=False), 2**18, loop=loop) + queue = WebSocketDataQueue( + mock.Mock(_reading_paused=False), DEFAULT_CHUNK_SIZE, loop=loop + ) reader = WebSocketReader(queue, 50000) writers = [] payloads = [] From 78487856ce091229e33267d700d7d3ff5495a857 Mon Sep 17 00:00:00 2001 From: Rui Xi Date: Tue, 14 Apr 2026 04:42:19 +0800 Subject: [PATCH 011/191] [PR #12264/af05010 backport][3.14] Reject HTTP/1.1 requests without Host header (#12367) (cherry picked from commit af05010f61ca9be00a98a0885c8ae213adf4cf45) --- CHANGES/10600.bugfix.rst | 2 + aiohttp/_http_parser.pyx | 7 +- aiohttp/http_parser.py | 5 +- tests/test_http_parser.py | 220 +++++++++++++++++++------------- tests/test_web_urldispatcher.py | 6 +- 5 files changed, 143 insertions(+), 97 deletions(-) create mode 100644 CHANGES/10600.bugfix.rst diff --git a/CHANGES/10600.bugfix.rst b/CHANGES/10600.bugfix.rst new file mode 100644 index 00000000000..eba47bf56e6 --- /dev/null +++ b/CHANGES/10600.bugfix.rst @@ -0,0 +1,2 @@ +Fixed http parser not rejecting HTTP/1.1 requests that do not have valid Host header. +-- by :user:`Cycloctane`. diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index e5e3bec8689..d8bb4f1c39f 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -457,6 +457,7 @@ cdef class HttpParser: cdef _on_headers_complete(self): self._process_header() + http_version = self.http_version() should_close = not cparser.llhttp_should_keep_alive(self._cparser) upgrade = self._cparser.upgrade chunked = self._cparser.flags & cparser.F_CHUNKED @@ -465,6 +466,8 @@ cdef class HttpParser: headers = CIMultiDictProxy(CIMultiDict(self._headers)) if self._cparser.type == cparser.HTTP_REQUEST: + if http_version == HttpVersion11 and hdrs.HOST not in headers: + raise BadHttpMessage("Missing 'Host' header in request.") h_upg = headers.get("upgrade", "") allowed = upgrade and h_upg.isascii() and h_upg.lower() in ALLOWED_UPGRADES if allowed or self._cparser.method == cparser.HTTP_CONNECT: @@ -488,11 +491,11 @@ cdef class HttpParser: method = http_method_str(self._cparser.method) msg = _new_request_message( method, self._path, - self.http_version(), headers, raw_headers, + http_version, headers, raw_headers, should_close, encoding, upgrade, chunked, self._url) else: msg = _new_response_message( - self.http_version(), self._cparser.status_code, self._reason, + http_version, self._cparser.status_code, self._reason, headers, raw_headers, should_close, encoding, upgrade, chunked) diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py index dce38f1c058..f3ea42f6e3f 100644 --- a/aiohttp/http_parser.py +++ b/aiohttp/http_parser.py @@ -49,7 +49,7 @@ LineTooLong, TransferEncodingError, ) -from .http_writer import HttpVersion, HttpVersion10 +from .http_writer import HttpVersion, HttpVersion10, HttpVersion11 from .streams import EMPTY_PAYLOAD, StreamReader from .typedefs import RawHeaders @@ -686,6 +686,9 @@ def parse_message(self, lines: list[bytes]) -> RawRequestMessage: chunked, ) = self.parse_headers(lines[1:]) + if version_o == HttpVersion11 and hdrs.HOST not in headers: + raise BadHttpMessage("Missing 'Host' header in request.") + if close is None: # then the headers weren't set in the request if version_o <= HttpVersion10: # HTTP 1.0 must asks to not close close = True diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index 9c65fb95257..c3a8669cb31 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -160,6 +160,7 @@ def test_c_parser_loaded(): def test_parse_headers(parser: Any) -> None: text = b"""GET /test HTTP/1.1\r +Host: a\r test: a line\r test2: data\r \r @@ -168,8 +169,16 @@ def test_parse_headers(parser: Any) -> None: assert len(messages) == 1 msg = messages[0][0] - assert list(msg.headers.items()) == [("test", "a line"), ("test2", "data")] - assert msg.raw_headers == ((b"test", b"a line"), (b"test2", b"data")) + assert list(msg.headers.items()) == [ + ("Host", "a"), + ("test", "a line"), + ("test2", "data"), + ] + assert msg.raw_headers == ( + (b"Host", b"a"), + (b"test", b"a line"), + (b"test2", b"data"), + ) assert not msg.should_close assert msg.compression is None assert not msg.upgrade @@ -177,6 +186,7 @@ def test_parse_headers(parser: Any) -> None: def test_reject_obsolete_line_folding(parser: Any) -> None: text = b"""GET /test HTTP/1.1\r +Host: a\r test: line\r Content-Length: 48\r test2: data\r @@ -251,7 +261,7 @@ def test_cve_2023_37276(parser: Any) -> None: r'"(),/:;<=>?@[\]{}', ) def test_bad_header_name(parser: Any, rfc9110_5_6_2_token_delim: str) -> None: - text = f"POST / HTTP/1.1\r\nhead{rfc9110_5_6_2_token_delim}er: val\r\n\r\n".encode() + text = f"POST / HTTP/1.1\r\nHost: a\r\nhead{rfc9110_5_6_2_token_delim}er: val\r\n\r\n".encode() expectation = pytest.raises(http_exceptions.BadHttpMessage) if rfc9110_5_6_2_token_delim == ":": # Inserting colon into header just splits name/value earlier. @@ -279,7 +289,7 @@ def test_bad_header_name(parser: Any, rfc9110_5_6_2_token_delim: str) -> None: ), ) def test_bad_headers(parser: Any, hdr: str) -> None: - text = f"POST / HTTP/1.1\r\n{hdr}\r\n\r\n".encode() + text = f"POST / HTTP/1.1\r\nHost: a\r\n{hdr}\r\n\r\n".encode() with pytest.raises(http_exceptions.BadHttpMessage): parser.feed_data(text) @@ -304,7 +314,7 @@ def test_unpaired_surrogate_in_header_py( max_field_size=8190, ) protocol._parser = parser - text = b"POST / HTTP/1.1\r\n\xff\r\n\r\n" + text = b"POST / HTTP/1.1\r\nHost: a\r\n\xff\r\n\r\n" message = None try: parser.feed_data(text) @@ -407,6 +417,12 @@ def test_duplicate_host_header_rejected(parser: HttpRequestParser) -> None: parser.feed_data(text) +def test_missing_host_header_rejected(parser: HttpRequestParser) -> None: + text = b"GET /admin HTTP/1.1\r\n\r\n" + with pytest.raises(http_exceptions.BadHttpMessage, match="Missing 'Host' header"): + parser.feed_data(text) + + @pytest.mark.parametrize( ("hdr1", "hdr2"), ( @@ -457,7 +473,7 @@ def test_bad_chunked(parser: HttpRequestParser) -> None: def test_whitespace_before_header(parser: Any) -> None: - text = b"GET / HTTP/1.1\r\n\tContent-Length: 1\r\n\r\nX" + text = b"GET / HTTP/1.1\r\nHost: a\r\n\tContent-Length: 1\r\n\r\nX" with pytest.raises(http_exceptions.BadHttpMessage): parser.feed_data(text) @@ -488,7 +504,7 @@ def test_parse_unusual_request_line(parser) -> None: def test_parse(parser) -> None: - text = b"GET /test HTTP/1.1\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\n\r\n" messages, upgrade, tail = parser.feed_data(text) assert len(messages) == 1 msg, _ = messages[0] @@ -497,10 +513,11 @@ def test_parse(parser) -> None: assert msg.method == "GET" assert msg.path == "/test" assert msg.version == (1, 1) + assert msg.headers["Host"] == "a" async def test_parse_body(parser) -> None: - text = b"GET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody" + text = b"GET /test HTTP/1.1\r\nHost: a\r\nContent-Length: 4\r\n\r\nbody" messages, upgrade, tail = parser.feed_data(text) assert len(messages) == 1 _, payload = messages[0] @@ -509,7 +526,7 @@ async def test_parse_body(parser) -> None: async def test_parse_body_with_CRLF(parser) -> None: - text = b"\r\nGET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody" + text = b"\r\nGET /test HTTP/1.1\r\nHost: a\r\nContent-Length: 4\r\n\r\nbody" messages, upgrade, tail = parser.feed_data(text) assert len(messages) == 1 _, payload = messages[0] @@ -518,7 +535,7 @@ async def test_parse_body_with_CRLF(parser) -> None: def test_parse_delayed(parser) -> None: - text = b"GET /test HTTP/1.1\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\n" messages, upgrade, tail = parser.feed_data(text) assert len(messages) == 0 assert not upgrade @@ -531,8 +548,9 @@ def test_parse_delayed(parser) -> None: def test_headers_multi_feed(parser) -> None: text1 = b"GET /test HTTP/1.1\r\n" - text2 = b"test: line" - text3 = b" continue\r\n\r\n" + text2 = b"Host: a\r\n" + text3 = b"test: line" + text4 = b" continue\r\n\r\n" messages, upgrade, tail = parser.feed_data(text1) assert len(messages) == 0 @@ -541,18 +559,21 @@ def test_headers_multi_feed(parser) -> None: assert len(messages) == 0 messages, upgrade, tail = parser.feed_data(text3) + assert len(messages) == 0 + + messages, upgrade, tail = parser.feed_data(text4) assert len(messages) == 1 msg = messages[0][0] - assert list(msg.headers.items()) == [("test", "line continue")] - assert msg.raw_headers == ((b"test", b"line continue"),) + assert list(msg.headers.items()) == [("Host", "a"), ("test", "line continue")] + assert msg.raw_headers == ((b"Host", b"a"), (b"test", b"line continue")) assert not msg.should_close assert msg.compression is None assert not msg.upgrade def test_headers_split_field(parser) -> None: - text1 = b"GET /test HTTP/1.1\r\n" + text1 = b"GET /test HTTP/1.1\r\nHost: a\r\n" text2 = b"t" text3 = b"es" text4 = b"t: value\r\n\r\n" @@ -565,8 +586,8 @@ def test_headers_split_field(parser) -> None: assert len(messages) == 1 msg = messages[0][0] - assert list(msg.headers.items()) == [("test", "value")] - assert msg.raw_headers == ((b"test", b"value"),) + assert list(msg.headers.items()) == [("Host", "a"), ("test", "value")] + assert msg.raw_headers == ((b"Host", b"a"), (b"test", b"value")) assert not msg.should_close assert msg.compression is None assert not msg.upgrade @@ -574,7 +595,7 @@ def test_headers_split_field(parser) -> None: def test_parse_headers_multi(parser) -> None: text = ( - b"GET /test HTTP/1.1\r\n" + b"GET /test HTTP/1.1\r\nHost: a\r\n" b"Set-Cookie: c1=cookie1\r\n" b"Set-Cookie: c2=cookie2\r\n\r\n" ) @@ -584,10 +605,12 @@ def test_parse_headers_multi(parser) -> None: msg = messages[0][0] assert list(msg.headers.items()) == [ + ("Host", "a"), ("Set-Cookie", "c1=cookie1"), ("Set-Cookie", "c2=cookie2"), ] assert msg.raw_headers == ( + (b"Host", b"a"), (b"Set-Cookie", b"c1=cookie1"), (b"Set-Cookie", b"c2=cookie2"), ) @@ -603,14 +626,14 @@ def test_conn_default_1_0(parser) -> None: def test_conn_default_1_1(parser) -> None: - text = b"GET /test HTTP/1.1\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\n\r\n" messages, upgrade, tail = parser.feed_data(text) msg = messages[0][0] assert not msg.should_close def test_conn_close(parser) -> None: - text = b"GET /test HTTP/1.1\r\nconnection: close\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\nconnection: close\r\n\r\n" messages, upgrade, tail = parser.feed_data(text) msg = messages[0][0] assert msg.should_close @@ -631,14 +654,14 @@ def test_conn_keep_alive_1_0(parser) -> None: def test_conn_keep_alive_1_1(parser) -> None: - text = b"GET /test HTTP/1.1\r\nconnection: keep-alive\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\nconnection: keep-alive\r\n\r\n" messages, upgrade, tail = parser.feed_data(text) msg = messages[0][0] assert not msg.should_close def test_conn_close_comma_list(parser) -> None: - text = b"GET /test HTTP/1.1\r\nconnection: close, keep-alive\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\nconnection: close, keep-alive\r\n\r\n" messages, upgrade, tail = parser.feed_data(text) msg = messages[0][0] assert msg.should_close @@ -647,6 +670,7 @@ def test_conn_close_comma_list(parser) -> None: def test_conn_close_multiple_headers(parser) -> None: text = ( b"GET /test HTTP/1.1\r\n" + b"Host: a\r\n" b"connection: keep-alive\r\n" b"connection: close\r\n\r\n" ) @@ -663,14 +687,14 @@ def test_conn_other_1_0(parser) -> None: def test_conn_other_1_1(parser) -> None: - text = b"GET /test HTTP/1.1\r\nconnection: test\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\nconnection: test\r\n\r\n" messages, upgrade, tail = parser.feed_data(text) msg = messages[0][0] assert not msg.should_close def test_request_chunked(parser) -> None: - text = b"GET /test HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\ntransfer-encoding: chunked\r\n\r\n" messages, upgrade, tail = parser.feed_data(text) msg, payload = messages[0] assert msg.chunked @@ -680,14 +704,14 @@ def test_request_chunked(parser) -> None: def test_te_header_non_ascii(parser: HttpRequestParser) -> None: # K = Kelvin sign, not valid ascii. - text = "GET /test HTTP/1.1\r\nTransfer-Encoding: chunKed\r\n\r\n" + text = "GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunKed\r\n\r\n" with pytest.raises(http_exceptions.BadHttpMessage): parser.feed_data(text.encode()) def test_upgrade_header_non_ascii(parser: HttpRequestParser) -> None: # K = Kelvin sign, not valid ascii. - text = "GET /test HTTP/1.1\r\nUpgrade: websocKet\r\n\r\n" + text = "GET /test HTTP/1.1\r\nHost: a\r\nUpgrade: websocKet\r\n\r\n" messages, upgrade, tail = parser.feed_data(text.encode()) assert not upgrade @@ -695,6 +719,7 @@ def test_upgrade_header_non_ascii(parser: HttpRequestParser) -> None: def test_request_te_chunked_with_content_length(parser: HttpRequestParser) -> None: text = ( b"GET /test HTTP/1.1\r\n" + b"Host: a\r\n" b"content-length: 1234\r\n" b"transfer-encoding: chunked\r\n\r\n" ) @@ -706,7 +731,7 @@ def test_request_te_chunked_with_content_length(parser: HttpRequestParser) -> No def test_request_te_chunked123(parser: Any) -> None: - text = b"GET /test HTTP/1.1\r\ntransfer-encoding: chunked123\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\ntransfer-encoding: chunked123\r\n\r\n" with pytest.raises( http_exceptions.BadHttpMessage, match="Request has invalid `Transfer-Encoding`", @@ -715,14 +740,14 @@ def test_request_te_chunked123(parser: Any) -> None: async def test_request_te_last_chunked(parser: Any) -> None: - text = b"GET /test HTTP/1.1\r\nTransfer-Encoding: not, chunked\r\n\r\n1\r\nT\r\n3\r\nest\r\n0\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: not, chunked\r\n\r\n1\r\nT\r\n3\r\nest\r\n0\r\n\r\n" messages, upgrade, tail = parser.feed_data(text) # https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.3 assert await messages[0][1].read() == b"Test" def test_request_te_first_chunked(parser: Any) -> None: - text = b"GET /test HTTP/1.1\r\nTransfer-Encoding: chunked, not\r\n\r\n1\r\nT\r\n3\r\nest\r\n0\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked, not\r\n\r\n1\r\nT\r\n3\r\nest\r\n0\r\n\r\n" # https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.3 with pytest.raises( http_exceptions.BadHttpMessage, @@ -734,6 +759,7 @@ def test_request_te_first_chunked(parser: Any) -> None: def test_conn_upgrade(parser: Any) -> None: text = ( b"GET /test HTTP/1.1\r\n" + b"Host: a\r\n" b"connection: upgrade\r\n" b"upgrade: websocket\r\n\r\n" ) @@ -747,6 +773,7 @@ def test_conn_upgrade(parser: Any) -> None: def test_conn_upgrade_comma_list(parser) -> None: text = ( b"GET /test HTTP/1.1\r\n" + b"host: a\r\n" b"connection: keep-alive, upgrade\r\n" b"upgrade: websocket\r\n\r\n" ) @@ -760,6 +787,7 @@ def test_conn_upgrade_comma_list(parser) -> None: def test_conn_upgrade_multiple_headers(parser) -> None: text = ( b"GET /test HTTP/1.1\r\n" + b"host: a\r\n" b"connection: keep-alive\r\n" b"connection: upgrade\r\n" b"upgrade: websocket\r\n\r\n" @@ -773,7 +801,7 @@ def test_conn_upgrade_multiple_headers(parser) -> None: def test_bad_upgrade(parser) -> None: """Test not upgraded if missing Upgrade header.""" - text = b"GET /test HTTP/1.1\r\nconnection: upgrade\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\nconnection: upgrade\r\n\r\n" messages, upgrade, tail = parser.feed_data(text) msg = messages[0][0] assert not msg.upgrade @@ -781,21 +809,21 @@ def test_bad_upgrade(parser) -> None: def test_compression_empty(parser) -> None: - text = b"GET /test HTTP/1.1\r\ncontent-encoding: \r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-encoding: \r\n\r\n" messages, upgrade, tail = parser.feed_data(text) msg = messages[0][0] assert msg.compression is None def test_compression_deflate(parser) -> None: - text = b"GET /test HTTP/1.1\r\ncontent-encoding: deflate\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-encoding: deflate\r\n\r\n" messages, upgrade, tail = parser.feed_data(text) msg = messages[0][0] assert msg.compression == "deflate" def test_compression_gzip(parser) -> None: - text = b"GET /test HTTP/1.1\r\ncontent-encoding: gzip\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-encoding: gzip\r\n\r\n" messages, upgrade, tail = parser.feed_data(text) msg = messages[0][0] assert msg.compression == "gzip" @@ -803,7 +831,7 @@ def test_compression_gzip(parser) -> None: @pytest.mark.skipif(brotli is None, reason="brotli is not installed") def test_compression_brotli(parser) -> None: - text = b"GET /test HTTP/1.1\r\ncontent-encoding: br\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-encoding: br\r\n\r\n" messages, upgrade, tail = parser.feed_data(text) msg = messages[0][0] assert msg.compression == "br" @@ -811,7 +839,7 @@ def test_compression_brotli(parser) -> None: @pytest.mark.skipif(zstandard is None, reason="zstandard is not installed") def test_compression_zstd(parser: HttpRequestParser) -> None: - text = b"GET /test HTTP/1.1\r\ncontent-encoding: zstd\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-encoding: zstd\r\n\r\n" messages, upgrade, tail = parser.feed_data(text) msg = messages[0][0] assert msg.compression == "zstd" @@ -825,7 +853,7 @@ def test_compression_zstd(parser: HttpRequestParser) -> None: ), ) def test_compression_non_ascii(parser: HttpRequestParser, enc: bytes) -> None: - text = b"GET /test HTTP/1.1\r\ncontent-encoding: " + enc + b"\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-encoding: " + enc + b"\r\n\r\n" messages, upgrade, tail = parser.feed_data(text) msg = messages[0][0] # Non-ascii input should not evaluate to a valid encoding scheme. @@ -833,14 +861,14 @@ def test_compression_non_ascii(parser: HttpRequestParser, enc: bytes) -> None: def test_compression_unknown(parser: HttpRequestParser) -> None: - text = b"GET /test HTTP/1.1\r\ncontent-encoding: compress\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-encoding: compress\r\n\r\n" messages, upgrade, tail = parser.feed_data(text) msg = messages[0][0] assert msg.compression is None def test_url_connect(parser: Any) -> None: - text = b"CONNECT www.google.com HTTP/1.1\r\ncontent-length: 0\r\n\r\n" + text = b"CONNECT www.google.com HTTP/1.1\r\nHost: a\r\ncontent-length: 0\r\n\r\n" messages, upgrade, tail = parser.feed_data(text) msg, payload = messages[0] assert upgrade @@ -848,7 +876,7 @@ def test_url_connect(parser: Any) -> None: def test_headers_connect(parser: Any) -> None: - text = b"CONNECT www.google.com HTTP/1.1\r\ncontent-length: 0\r\n\r\n" + text = b"CONNECT www.google.com HTTP/1.1\r\nHost: a\r\ncontent-length: 0\r\n\r\n" messages, upgrade, tail = parser.feed_data(text) msg, payload = messages[0] assert upgrade @@ -858,6 +886,7 @@ def test_headers_connect(parser: Any) -> None: def test_url_absolute(parser: Any) -> None: text = ( b"GET https://www.google.com/path/to.html HTTP/1.1\r\n" + b"Host: a\r\n" b"content-length: 0\r\n\r\n" ) messages, upgrade, tail = parser.feed_data(text) @@ -868,21 +897,21 @@ def test_url_absolute(parser: Any) -> None: def test_headers_old_websocket_key1(parser: Any) -> None: - text = b"GET /test HTTP/1.1\r\nSEC-WEBSOCKET-KEY1: line\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\nSEC-WEBSOCKET-KEY1: line\r\n\r\n" with pytest.raises(http_exceptions.BadHttpMessage): parser.feed_data(text) def test_headers_content_length_err_1(parser) -> None: - text = b"GET /test HTTP/1.1\r\ncontent-length: line\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-length: line\r\n\r\n" with pytest.raises(http_exceptions.BadHttpMessage): parser.feed_data(text) def test_headers_content_length_err_2(parser) -> None: - text = b"GET /test HTTP/1.1\r\ncontent-length: -1\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-length: -1\r\n\r\n" with pytest.raises(http_exceptions.BadHttpMessage): parser.feed_data(text) @@ -905,7 +934,7 @@ def test_headers_content_length_err_2(parser) -> None: @pytest.mark.parametrize("pad2", _pad.keys(), ids=["post-" + n for n in _pad.values()]) @pytest.mark.parametrize("pad1", _pad.keys(), ids=["pre-" + n for n in _pad.values()]) def test_invalid_header_spacing(parser, pad1: bytes, pad2: bytes, hdr: bytes) -> None: - text = b"GET /test HTTP/1.1\r\n%s%s%s: value\r\n\r\n" % (pad1, hdr, pad2) + text = b"GET /test HTTP/1.1\r\nHost: a\r\n%s%s%s: value\r\n\r\n" % (pad1, hdr, pad2) expectation = pytest.raises(http_exceptions.BadHttpMessage) if pad1 == pad2 == b"" and hdr != b"": # one entry in param matrix is correct: non-empty name, not padded @@ -915,19 +944,19 @@ def test_invalid_header_spacing(parser, pad1: bytes, pad2: bytes, hdr: bytes) -> def test_empty_header_name(parser) -> None: - text = b"GET /test HTTP/1.1\r\n:test\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\n:test\r\n\r\n" with pytest.raises(http_exceptions.BadHttpMessage): parser.feed_data(text) def test_invalid_header(parser) -> None: - text = b"GET /test HTTP/1.1\r\ntest line\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\ntest line\r\n\r\n" with pytest.raises(http_exceptions.BadHttpMessage): parser.feed_data(text) def test_invalid_name(parser) -> None: - text = b"GET /test HTTP/1.1\r\ntest[]: line\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\ntest[]: line\r\n\r\n" with pytest.raises(http_exceptions.BadHttpMessage): parser.feed_data(text) @@ -946,15 +975,15 @@ def test_max_header_field_size(parser, size) -> None: def test_max_header_size_under_limit(parser: HttpRequestParser) -> None: name = b"t" * 8185 - text = b"GET /test HTTP/1.1\r\n" + name + b":data\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\n" + name + b":data\r\n\r\n" messages, upgrade, tail = parser.feed_data(text) msg = messages[0][0] assert msg.method == "GET" assert msg.path == "/test" assert msg.version == (1, 1) - assert msg.headers == CIMultiDict({name.decode(): "data"}) - assert msg.raw_headers == ((name, b"data"),) + assert msg.headers == CIMultiDict([("Host", "a"), (name.decode(), "data")]) + assert msg.raw_headers == ((b"Host", b"a"), (name, b"data")) assert not msg.should_close assert msg.compression is None assert not msg.upgrade @@ -986,7 +1015,7 @@ def test_max_header_combined_size(parser: HttpRequestParser) -> None: async def test_max_trailer_size(parser: HttpRequestParser, size: int) -> None: value = b"t" * size text = ( - b"GET /test HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n" + b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\n\r\n" + hex(4000)[2:].encode() + b"\r\n" + b"b" * 4000 @@ -1012,7 +1041,7 @@ async def test_max_headers( parser: HttpRequestParser, headers: int, trailers: int ) -> None: text = ( - b"GET /test HTTP/1.1\r\nTransfer-Encoding: chunked" + b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked" + b"".join(b"\r\nHeader-%d: Value" % i for i in range(headers)) + b"\r\n\r\n4\r\ntest\r\n0" + b"".join(b"\r\nTrailer-%d: Value" % i for i in range(trailers)) @@ -1028,15 +1057,15 @@ async def test_max_headers( def test_max_header_value_size_under_limit(parser: HttpRequestParser) -> None: value = b"A" * 8185 - text = b"GET /test HTTP/1.1\r\ndata:" + value + b"\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\ndata:" + value + b"\r\n\r\n" messages, upgrade, tail = parser.feed_data(text) msg = messages[0][0] assert msg.method == "GET" assert msg.path == "/test" assert msg.version == (1, 1) - assert msg.headers == CIMultiDict({"data": value.decode()}) - assert msg.raw_headers == ((b"data", value),) + assert msg.headers == CIMultiDict([("Host", "a"), ("data", value.decode())]) + assert msg.raw_headers == ((b"Host", b"a"), (b"data", value)) assert not msg.should_close assert msg.compression is None assert not msg.upgrade @@ -1046,7 +1075,7 @@ def test_max_header_value_size_under_limit(parser: HttpRequestParser) -> None: async def test_chunk_splits_after_pause(parser: HttpRequestParser) -> None: text = ( - b"GET /test HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n" + b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\n\r\n" + b"1\r\nb\r\n" * 50000 + b"0\r\n\r\n" ) @@ -1272,15 +1301,15 @@ def test_max_header_value_size_continuation_under_limit( def test_http_request_parser(parser) -> None: - text = b"GET /path HTTP/1.1\r\n\r\n" + text = b"GET /path HTTP/1.1\r\nHost: a\r\n\r\n" messages, upgrade, tail = parser.feed_data(text) msg = messages[0][0] assert msg.method == "GET" assert msg.path == "/path" assert msg.version == (1, 1) - assert msg.headers == CIMultiDict() - assert msg.raw_headers == () + assert msg.headers == CIMultiDict({"Host": "a"}) + assert msg.raw_headers == ((b"Host", b"a"),) assert not msg.should_close assert msg.compression is None assert not msg.upgrade @@ -1311,7 +1340,7 @@ def test_http_request_bad_status_line(parser) -> None: def test_http_request_bad_status_line_number( parser: Any, nonascii_digit: bytes ) -> None: - text = b"GET /digit HTTP/1." + nonascii_digit + b"\r\n\r\n" + text = b"GET /digit HTTP/1." + nonascii_digit + b"\r\nHost: a\r\n\r\n" with pytest.raises(http_exceptions.BadStatusLine): parser.feed_data(text) @@ -1319,19 +1348,19 @@ def test_http_request_bad_status_line_number( def test_http_request_bad_status_line_separator(parser: Any) -> None: # single code point, old, multibyte NFKC, multibyte NFKD utf8sep = "\N{arabic ligature sallallahou alayhe wasallam}".encode() - text = b"GET /ligature HTTP/1" + utf8sep + b"1\r\n\r\n" + text = b"GET /ligature HTTP/1" + utf8sep + b"1\r\nHost: a\r\n\r\n" with pytest.raises(http_exceptions.BadStatusLine): parser.feed_data(text) def test_http_request_bad_status_line_whitespace(parser: Any) -> None: - text = b"GET\n/path\fHTTP/1.1\r\n\r\n" + text = b"GET\n/path\fHTTP/1.1\r\nHost: a\r\n\r\n" with pytest.raises(http_exceptions.BadStatusLine): parser.feed_data(text) def test_http_request_message_after_close(parser: HttpRequestParser) -> None: - text = b"GET / HTTP/1.1\r\nConnection: close\r\n\r\nInvalid\r\n\r\n" + text = b"GET / HTTP/1.1\r\nHost: a\r\nConnection: close\r\n\r\nInvalid\r\n\r\n" with pytest.raises( http_exceptions.BadHttpMessage, match="Data after `Connection: close`" ): @@ -1339,7 +1368,7 @@ def test_http_request_message_after_close(parser: HttpRequestParser) -> None: def test_http_request_message_after_close_comma_list(parser: HttpRequestParser) -> None: - text = b"GET / HTTP/1.1\r\nConnection: close, keep-alive\r\n\r\nInvalid\r\n\r\n" + text = b"GET / HTTP/1.1\r\nHost: a\r\nConnection: close, keep-alive\r\n\r\nInvalid\r\n\r\n" with pytest.raises( http_exceptions.BadHttpMessage, match="Data after `Connection: close`" ): @@ -1349,6 +1378,7 @@ def test_http_request_message_after_close_comma_list(parser: HttpRequestParser) def test_http_request_upgrade(parser: HttpRequestParser) -> None: text = ( b"GET /test HTTP/1.1\r\n" + b"Host: a\r\n" b"connection: upgrade\r\n" b"upgrade: websocket\r\n\r\n" b"some raw data" @@ -1364,6 +1394,7 @@ def test_http_request_upgrade(parser: HttpRequestParser) -> None: async def test_http_request_upgrade_unknown(parser: Any) -> None: text = ( b"POST / HTTP/1.1\r\n" + b"Host: a\r\n" b"Connection: Upgrade\r\n" b"Content-Length: 2\r\n" b"Upgrade: unknown\r\n" @@ -1397,7 +1428,7 @@ def xfail_c_parser_url(request) -> None: def test_http_request_parser_utf8_request_line(parser) -> None: messages, upgrade, tail = parser.feed_data( # note the truncated unicode sequence - b"GET /P\xc3\xbcnktchen\xa0\xef\xb7 HTTP/1.1\r\n" + + b"GET /P\xc3\xbcnktchen\xa0\xef\xb7 HTTP/1.1\r\nHost: a\r\n" + # for easier grep: ASCII 0xA0 more commonly known as non-breaking space # note the leading and trailing spaces "sTeP: \N{latin small letter sharp s}nek\t\N{no-break space} " @@ -1408,8 +1439,8 @@ def test_http_request_parser_utf8_request_line(parser) -> None: assert msg.method == "GET" assert msg.path == "/Pünktchen\udca0\udcef\udcb7" assert msg.version == (1, 1) - assert msg.headers == CIMultiDict([("STEP", "ßnek\t\xa0")]) - assert msg.raw_headers == ((b"sTeP", "ßnek\t\xa0".encode()),) + assert msg.headers == CIMultiDict([("Host", "a"), ("STEP", "ßnek\t\xa0")]) + assert msg.raw_headers == ((b"Host", b"a"), (b"sTeP", "ßnek\t\xa0".encode())) assert not msg.should_close assert msg.compression is None assert not msg.upgrade @@ -1420,15 +1451,15 @@ def test_http_request_parser_utf8_request_line(parser) -> None: def test_http_request_parser_utf8(parser) -> None: - text = "GET /path HTTP/1.1\r\nx-test:тест\r\n\r\n".encode() + text = "GET /path HTTP/1.1\r\nHost: a\r\nx-test:тест\r\n\r\n".encode() messages, upgrade, tail = parser.feed_data(text) msg = messages[0][0] assert msg.method == "GET" assert msg.path == "/path" assert msg.version == (1, 1) - assert msg.headers == CIMultiDict([("X-TEST", "тест")]) - assert msg.raw_headers == ((b"x-test", "тест".encode()),) + assert msg.headers == CIMultiDict([("Host", "a"), ("X-TEST", "тест")]) + assert msg.raw_headers == ((b"Host", b"a"), (b"x-test", "тест".encode())) assert not msg.should_close assert msg.compression is None assert not msg.upgrade @@ -1437,16 +1468,19 @@ def test_http_request_parser_utf8(parser) -> None: def test_http_request_parser_non_utf8(parser) -> None: - text = "GET /path HTTP/1.1\r\nx-test:тест\r\n\r\n".encode("cp1251") + text = "GET /path HTTP/1.1\r\nHost: a\r\nx-test:тест\r\n\r\n".encode("cp1251") msg = parser.feed_data(text)[0][0][0] assert msg.method == "GET" assert msg.path == "/path" assert msg.version == (1, 1) assert msg.headers == CIMultiDict( - [("X-TEST", "тест".encode("cp1251").decode("utf8", "surrogateescape"))] + [ + ("Host", "a"), + ("X-TEST", "тест".encode("cp1251").decode("utf8", "surrogateescape")), + ] ) - assert msg.raw_headers == ((b"x-test", "тест".encode("cp1251")),) + assert msg.raw_headers == ((b"Host", b"a"), (b"x-test", "тест".encode("cp1251"))) assert not msg.should_close assert msg.compression is None assert not msg.upgrade @@ -1455,7 +1489,7 @@ def test_http_request_parser_non_utf8(parser) -> None: def test_http_request_parser_two_slashes(parser) -> None: - text = b"GET //path HTTP/1.1\r\n\r\n" + text = b"GET //path HTTP/1.1\r\nHost: a\r\n\r\n" msg = parser.feed_data(text)[0][0][0] assert msg.method == "GET" @@ -1476,17 +1510,19 @@ def test_http_request_parser_bad_method( parser, rfc9110_5_6_2_token_delim: bytes ) -> None: with pytest.raises(http_exceptions.BadHttpMethod): - parser.feed_data(rfc9110_5_6_2_token_delim + b'ET" /get HTTP/1.1\r\n\r\n') + parser.feed_data( + rfc9110_5_6_2_token_delim + b'ET" /get HTTP/1.1\r\nHost: a\r\n\r\n' + ) def test_http_request_parser_bad_version(parser) -> None: with pytest.raises(http_exceptions.BadHttpMessage): - parser.feed_data(b"GET //get HT/11\r\n\r\n") + parser.feed_data(b"GET //get HT/11\r\nHost: a\r\n\r\n") def test_http_request_parser_bad_version_number(parser: Any) -> None: with pytest.raises(http_exceptions.BadHttpMessage): - parser.feed_data(b"GET /test HTTP/1.32\r\n\r\n") + parser.feed_data(b"GET /test HTTP/1.32\r\nHost: a\r\n\r\n") def test_http_request_parser_bad_ascii_uri(parser: Any) -> None: @@ -1510,15 +1546,15 @@ def test_http_request_max_status_line(parser, size) -> None: def test_http_request_max_status_line_under_limit(parser: HttpRequestParser) -> None: path = b"t" * 8172 messages, upgraded, tail = parser.feed_data( - b"GET /path" + path + b" HTTP/1.1\r\n\r\n" + b"GET /path" + path + b" HTTP/1.1\r\nHost: a\r\n\r\n" ) msg = messages[0][0] assert msg.method == "GET" assert msg.path == "/path" + path.decode() assert msg.version == (1, 1) - assert msg.headers == CIMultiDict() - assert msg.raw_headers == () + assert msg.headers == CIMultiDict({"Host": "a"}) + assert msg.raw_headers == ((b"Host", b"a"),) assert not msg.should_close assert msg.compression is None assert not msg.upgrade @@ -1755,7 +1791,7 @@ def test_http_response_parser_code_not_ascii(response, nonascii_digit: bytes) -> def test_http_request_chunked_payload(parser) -> None: - text = b"GET /test HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\ntransfer-encoding: chunked\r\n\r\n" msg, payload = parser.feed_data(text)[0][0] assert msg.chunked @@ -1771,12 +1807,13 @@ def test_http_request_chunked_payload(parser) -> None: def test_http_request_chunked_payload_and_next_message(parser) -> None: - text = b"GET /test HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\ntransfer-encoding: chunked\r\n\r\n" msg, payload = parser.feed_data(text)[0][0] messages, upgraded, tail = parser.feed_data( b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n" b"POST /test2 HTTP/1.1\r\n" + b"Host: a\r\n" b"transfer-encoding: chunked\r\n\r\n" ) @@ -1794,7 +1831,7 @@ def test_http_request_chunked_payload_and_next_message(parser) -> None: def test_http_request_chunked_payload_chunks(parser) -> None: - text = b"GET /test HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\ntransfer-encoding: chunked\r\n\r\n" msg, payload = parser.feed_data(text)[0][0] parser.feed_data(b"4\r\ndata\r") @@ -1817,7 +1854,7 @@ def test_http_request_chunked_payload_chunks(parser) -> None: def test_parse_chunked_payload_chunk_extension(parser) -> None: - text = b"GET /test HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\ntransfer-encoding: chunked\r\n\r\n" msg, payload = parser.feed_data(text)[0][0] parser.feed_data(b"4;test\r\ndata\r\n4\r\nline\r\n0\r\ntest: test\r\n\r\n") @@ -1829,7 +1866,7 @@ def test_parse_chunked_payload_chunk_extension(parser) -> None: async def test_request_chunked_with_trailer(parser: HttpRequestParser) -> None: - text = b"GET /test HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ntest\r\n0\r\ntest: trailer\r\nsecond: test trailer\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ntest\r\n0\r\ntest: trailer\r\nsecond: test trailer\r\n\r\n" messages, upgraded, tail = parser.feed_data(text) assert not tail msg, payload = messages[0] @@ -1839,7 +1876,7 @@ async def test_request_chunked_with_trailer(parser: HttpRequestParser) -> None: async def test_request_chunked_reject_bad_trailer(parser: HttpRequestParser) -> None: - text = b"GET /test HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n0\r\nbad\ntrailer\r\n\r\n" + text = b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\n\r\n0\r\nbad\ntrailer\r\n\r\n" with pytest.raises(http_exceptions.BadHttpMessage, match=r"b'bad\\ntrailer'"): parser.feed_data(text) @@ -1852,7 +1889,7 @@ def test_parse_no_length_or_te_on_post( protocol = RequestHandler(server, loop=loop) parser = request_cls(protocol, loop, limit=DEFAULT_CHUNK_SIZE) protocol._parser = parser - text = b"POST /test HTTP/1.1\r\n\r\n" + text = b"POST /test HTTP/1.1\r\nHost: a\r\n\r\n" msg, payload = parser.feed_data(text)[0][0] assert payload.is_eof() @@ -1885,7 +1922,7 @@ def test_parse_length_payload(response) -> None: def test_parse_no_length_payload(parser) -> None: - text = b"PUT / HTTP/1.1\r\n\r\n" + text = b"PUT / HTTP/1.1\r\nHost: a\r\n\r\n" msg, payload = parser.feed_data(text)[0][0] assert payload.is_eof() @@ -2055,7 +2092,7 @@ async def test_parse_chunked_payload_with_lf_in_extensions( def test_partial_url(parser: HttpRequestParser) -> None: messages, upgrade, tail = parser.feed_data(b"GET /te") assert len(messages) == 0 - messages, upgrade, tail = parser.feed_data(b"st HTTP/1.1\r\n\r\n") + messages, upgrade, tail = parser.feed_data(b"st HTTP/1.1\r\nHost: a\r\n\r\n") assert len(messages) == 1 msg, payload = messages[0] @@ -2078,7 +2115,7 @@ def test_partial_url(parser: HttpRequestParser) -> None: ], ) def test_parse_uri_percent_encoded(parser, uri, path, query, fragment) -> None: - text = (f"GET {uri} HTTP/1.1\r\n\r\n").encode() + text = (f"GET {uri} HTTP/1.1\r\nHost: a\r\n\r\n").encode() messages, upgrade, tail = parser.feed_data(text) msg = messages[0][0] @@ -2092,7 +2129,7 @@ def test_parse_uri_percent_encoded(parser, uri, path, query, fragment) -> None: def test_parse_uri_utf8(parser) -> None: if not isinstance(parser, HttpRequestParserPy): pytest.xfail("Not valid HTTP. Maybe update py-parser to reject later.") - text = ("GET /путь?ключ=знач#фраг HTTP/1.1\r\n\r\n").encode() + text = ("GET /путь?ключ=знач#фраг HTTP/1.1\r\nHost: a\r\n\r\n").encode() messages, upgrade, tail = parser.feed_data(text) msg = messages[0][0] @@ -2104,7 +2141,8 @@ def test_parse_uri_utf8(parser) -> None: def test_parse_uri_utf8_percent_encoded(parser) -> None: text = ( - "GET %s HTTP/1.1\r\n\r\n" % quote("/путь?ключ=знач#фраг", safe="/?=#") + "GET %s HTTP/1.1\r\nHost: a\r\n\r\n" + % quote("/путь?ключ=знач#фраг", safe="/?=#") ).encode() messages, upgrade, tail = parser.feed_data(text) msg = messages[0][0] diff --git a/tests/test_web_urldispatcher.py b/tests/test_web_urldispatcher.py index 85138a343b0..05f028a88e4 100644 --- a/tests/test_web_urldispatcher.py +++ b/tests/test_web_urldispatcher.py @@ -249,7 +249,7 @@ async def test_follow_symlink_directory_traversal( # We need to use a raw socket to test this, as the client will normalize # the path before sending it to the server. reader, writer = await asyncio.open_connection(client.host, client.port) - writer.write(b"GET /../private_file HTTP/1.1\r\n\r\n") + writer.write(b"GET /../private_file HTTP/1.1\r\nHost: a\r\n\r\n") response = await reader.readuntil(b"\r\n\r\n") assert b"404 Not Found" in response writer.close() @@ -301,14 +301,14 @@ async def test_follow_symlink_directory_traversal_after_normalization( # We need to use a raw socket to test this, as the client will normalize # the path before sending it to the server. reader, writer = await asyncio.open_connection(client.host, client.port) - writer.write(b"GET /my_symlink/../private_file HTTP/1.1\r\n\r\n") + writer.write(b"GET /my_symlink/../private_file HTTP/1.1\r\nHost: a\r\n\r\n") response = await reader.readuntil(b"\r\n\r\n") assert b"404 Not Found" in response writer.close() await writer.wait_closed() reader, writer = await asyncio.open_connection(client.host, client.port) - writer.write(b"GET /my_symlink/symlink_target_file HTTP/1.1\r\n\r\n") + writer.write(b"GET /my_symlink/symlink_target_file HTTP/1.1\r\nHost: a\r\n\r\n") response = await reader.readuntil(b"\r\n\r\n") assert b"200 OK" in response response = await reader.readuntil(b"readable") From 0544166ae26ed6e1f5bb63def9de6726205b125a Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 22:31:39 +0100 Subject: [PATCH 012/191] [PR #12364/3f772cf7 backport][3.14] Disable cov/xdist by default (#12368) **This is a backport of PR #12364 as merged into master (3f772cf70b552671c5ffca0a7e8b308fca323a25).** Co-authored-by: Sam Bull --- .github/workflows/ci-cd.yml | 13 +++++++------ CHANGES/12364.contrib.rst | 1 + setup.cfg | 8 -------- 3 files changed, 8 insertions(+), 14 deletions(-) create mode 100644 CHANGES/12364.contrib.rst diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 34488f65681..bb87dc5de62 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -222,7 +222,8 @@ jobs: PIP_USER: 1 run: >- PATH="${HOME}/Library/Python/3.11/bin:${HOME}/.local/bin:${PATH}" - pytest --junitxml=junit.xml -m 'not dev_mode and not autobahn' + pytest --junitxml=junit.xml --numprocesses=auto --cov=aiohttp/ --cov=tests/ + -m 'not dev_mode and not autobahn' shell: bash - name: Re-run the failing tests with maximum verbosity if: failure() @@ -230,7 +231,7 @@ jobs: COLOR: yes AIOHTTP_NO_EXTENSIONS: ${{ matrix.no-extensions }} run: >- # `exit 1` makes sure that the job remains red with flaky runs - pytest --no-cov --numprocesses=0 -vvvvv --lf && exit 1 + pytest --no-cov -vvvvv --lf && exit 1 shell: bash - name: Run dev_mode tests env: @@ -238,7 +239,7 @@ jobs: AIOHTTP_NO_EXTENSIONS: ${{ matrix.no-extensions }} PIP_USER: 1 PYTHONDEVMODE: 1 - run: pytest -m dev_mode --cov-append --numprocesses=0 + run: pytest -m dev_mode --cov=aiohttp/ --cov=tests/ --cov-append shell: bash - name: Turn coverage into xml env: @@ -330,7 +331,7 @@ jobs: PIP_USER: 1 run: >- PATH="${HOME}/Library/Python/3.11/bin:${HOME}/.local/bin:${PATH}" - pytest --junitxml=junit.xml --numprocesses=0 -m autobahn + pytest --junitxml=junit.xml --cov=aiohttp/ --cov=tests/ -m autobahn shell: bash - name: Turn coverage into xml env: @@ -398,7 +399,7 @@ jobs: uses: CodSpeedHQ/action@v4 with: mode: instrumentation - run: python -Im pytest --no-cov --numprocesses=0 -vvvvv --codspeed + run: python -Im pytest --no-cov -vvvvv --codspeed cython-coverage: @@ -447,7 +448,7 @@ jobs: PIP_USER: 1 run: >- pytest tests/test_client_functional.py tests/test_http_parser.py tests/test_http_writer.py tests/test_web_functional.py tests/test_web_response.py tests/test_websocket_parser.py - --cov-config=.coveragerc-cython.toml + --cov-config=.coveragerc-cython.toml --cov=aiohttp/ --cov=tests/ --numprocesses=auto -m 'not dev_mode and not autobahn' shell: bash - name: Turn coverage into xml diff --git a/CHANGES/12364.contrib.rst b/CHANGES/12364.contrib.rst new file mode 100644 index 00000000000..21b9eb1b271 --- /dev/null +++ b/CHANGES/12364.contrib.rst @@ -0,0 +1 @@ +Disabled ``coverage`` and ``xdist`` by default to ease local development -- by :user:`Dreamsorcerer`. diff --git a/setup.cfg b/setup.cfg index 01e43450d95..62f68b11eab 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,9 +41,6 @@ exclude_lines = [tool:pytest] addopts = - # `pytest-xdist`: - --numprocesses=auto - # show 10 slowest invocations: --durations=10 @@ -56,11 +53,6 @@ addopts = # show values of the local vars in errors: --showlocals - # `pytest-cov`: - -p pytest_cov - --cov=aiohttp - --cov=tests/ - -m "not dev_mode and not autobahn and not internal" filterwarnings = error From 3c04f9fc168c73c0363cb0cfe6b4e43f97e1cfa8 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 00:07:05 +0100 Subject: [PATCH 013/191] [PR #12202/07bd8c1d backport][3.14] docs: clarify params query canonicalization behavior (#12369) **This is a backport of PR #12202 as merged into master (07bd8c1d36d19e5ee38146d44607d758a81893b3).** Co-authored-by: Dr Alex Mitre --- docs/client_quickstart.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/client_quickstart.rst b/docs/client_quickstart.rst index d00333c1e48..2c6b50941d2 100644 --- a/docs/client_quickstart.rst +++ b/docs/client_quickstart.rst @@ -119,8 +119,9 @@ that case you can specify multiple values for each key:: expect = 'http://httpbin.org/get?key=value2&key=value1' assert str(r.url) == expect -You can also pass :class:`str` content as param, but beware -- content -is not encoded by library. Note that ``+`` is not encoded:: +You can also pass :class:`str` content as param. The value is used as a +query string, but passing ``params`` does not disable URL +canonicalization. Note that ``+`` is not encoded:: async with session.get('http://httpbin.org/get', params='key=value+1') as r: @@ -146,7 +147,9 @@ is not encoded by library. Note that ``+`` is not encoded:: .. warning:: - Passing *params* overrides ``encoded=True``, never use both options. + Passing *params* overrides ``encoded=True``. Never use both options + if you need to preserve exact query-string bytes. + Build the full URL (including query) instead. Response Content and Status Code ================================ From 79f438dcbb1933e378d96d79ef65a1b32dab416b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:07:06 +0000 Subject: [PATCH 014/191] Bump actions/cache from 5.0.4 to 5.0.5 (#12373) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/cache](https://github.com/actions/cache) from 5.0.4 to 5.0.5.
Release notes

Sourced from actions/cache's releases.

v5.0.5

What's Changed

Full Changelog: https://github.com/actions/cache/compare/v5...v5.0.5

Changelog

Sourced from actions/cache's changelog.

Releases

How to prepare a release

[!NOTE]
Relevant for maintainers with write access only.

  1. Switch to a new branch from main.
  2. Run npm test to ensure all tests are passing.
  3. Update the version in https://github.com/actions/cache/blob/main/package.json.
  4. Run npm run build to update the compiled files.
  5. Update this https://github.com/actions/cache/blob/main/RELEASES.md with the new version and changes in the ## Changelog section.
  6. Run licensed cache to update the license report.
  7. Run licensed status and resolve any warnings by updating the https://github.com/actions/cache/blob/main/.licensed.yml file with the exceptions.
  8. Commit your changes and push your branch upstream.
  9. Open a pull request against main and get it reviewed and merged.
  10. Draft a new release https://github.com/actions/cache/releases use the same version number used in package.json
    1. Create a new tag with the version number.
    2. Auto generate release notes and update them to match the changes you made in RELEASES.md.
    3. Toggle the set as the latest release option.
    4. Publish the release.
  11. Navigate to https://github.com/actions/cache/actions/workflows/release-new-action-version.yml
    1. There should be a workflow run queued with the same version number.
    2. Approve the run to publish the new version and update the major tags for this action.

Changelog

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/cache&package-manager=github_actions&previous-version=5.0.4&new-version=5.0.5)](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> --- .github/workflows/ci-cd.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index bb87dc5de62..f4c6b972cf7 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -71,7 +71,7 @@ jobs: with: python-version: 3.11 - name: Cache PyPI - uses: actions/cache@v5.0.4 + uses: actions/cache@v5.0.5 with: key: pip-lint-${{ hashFiles('requirements/*.txt') }} path: ~/.cache/pip @@ -120,7 +120,7 @@ jobs: with: submodules: true - name: Cache llhttp generated files - uses: actions/cache@v5.0.4 + uses: actions/cache@v5.0.5 id: cache with: key: llhttp-${{ hashFiles('vendor/llhttp/package*.json', 'vendor/llhttp/src/**/*') }} @@ -184,7 +184,7 @@ jobs: echo "dir=$(pip cache dir)" >> "${GITHUB_OUTPUT}" shell: bash - name: Cache PyPI - uses: actions/cache@v5.0.4 + uses: actions/cache@v5.0.5 with: key: pip-ci-${{ runner.os }}-${{ matrix.pyver }}-${{ matrix.no-extensions }}-${{ hashFiles('requirements/*.txt') }} path: ${{ steps.pip-cache.outputs.dir }} @@ -296,7 +296,7 @@ jobs: echo "dir=$(pip cache dir)" >> "${GITHUB_OUTPUT}" shell: bash - name: Cache PyPI - uses: actions/cache@v5.0.4 + uses: actions/cache@v5.0.5 with: key: pip-ci-${{ runner.os }}-${{ matrix.pyver }}-${{ matrix.no-extensions }}-${{ hashFiles('requirements/*.txt') }} path: ${{ steps.pip-cache.outputs.dir }} From ceca3a194cbde8c5253cd28eb9460490b9d0165a Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:36:00 +0100 Subject: [PATCH 015/191] [PR #12332/06e510b2 backport][3.14] Security: Silent Exception Swallowing in Server Request Handler Factory (#12370) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **This is a backport of PR #12332 as merged into master (06e510b2be211f2a1e3a37ed7bea0a3b5cf2906e).** Signed-off-by: Trần Bách <45133811+barttran2k@users.noreply.github.com> Co-authored-by: Trần Bách <45133811+barttran2k@users.noreply.github.com> Co-authored-by: Sam Bull --- aiohttp/web_server.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/aiohttp/web_server.py b/aiohttp/web_server.py index 778e6cf81c4..97cf2a60a91 100644 --- a/aiohttp/web_server.py +++ b/aiohttp/web_server.py @@ -81,4 +81,11 @@ def __call__(self) -> RequestHandler: for k, v in self._kwargs.items() if k in ["debug", "access_log_class"] } - return RequestHandler(self, loop=self._loop, **kwargs) + handler = RequestHandler(self, loop=self._loop, **kwargs) + handler.logger.warning( + "Failed to create request handler with custom kwargs %r, " + "falling back to filtered kwargs. This may indicate a " + "misconfiguration.", + self._kwargs, + ) + return handler From 5c17b642a0b918b99e8fc335ecc1e041eaeabab9 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Wed, 15 Apr 2026 19:16:28 +0100 Subject: [PATCH 016/191] Benchmark tests for decompression optimisations (#12358) (#12381) (cherry picked from commit adf7799b26d400f0a1e9e28b9d5928792c86269f) --- CHANGES/12358.misc.rst | 1 + aiohttp/web_response.py | 12 ++------ tests/test_benchmarks_client.py | 30 ++++++++++++++++++++ tests/test_benchmarks_web_request.py | 42 ++++++++++++++++++++++++++++ tests/test_web_response.py | 20 ------------- 5 files changed, 75 insertions(+), 30 deletions(-) create mode 100644 CHANGES/12358.misc.rst create mode 100644 tests/test_benchmarks_web_request.py diff --git a/CHANGES/12358.misc.rst b/CHANGES/12358.misc.rst new file mode 100644 index 00000000000..7b035023500 --- /dev/null +++ b/CHANGES/12358.misc.rst @@ -0,0 +1 @@ +Changed ``zlib_executor_size`` default so compressed payloads are async by default -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/web_response.py b/aiohttp/web_response.py index 91abbb4b61c..7a14ada4dbb 100644 --- a/aiohttp/web_response.py +++ b/aiohttp/web_response.py @@ -16,7 +16,7 @@ from . import hdrs, payload from .abc import AbstractStreamWriter -from .compression_utils import ZLibCompressor +from .compression_utils import MAX_SYNC_CHUNK_SIZE, ZLibCompressor from .helpers import ( ETAG_ANY, QUOTED_ETAG_RE, @@ -35,7 +35,6 @@ from .typedefs import JSONBytesEncoder, JSONEncoder, LooseHeaders REASON_PHRASES = {http_status.value: http_status.phrase for http_status in HTTPStatus} -LARGE_BODY_SIZE = 1024**2 __all__ = ( "ContentCoding", @@ -665,7 +664,7 @@ def __init__( headers: LooseHeaders | None = None, content_type: str | None = None, charset: str | None = None, - zlib_executor_size: int | None = None, + zlib_executor_size: int = MAX_SYNC_CHUNK_SIZE, zlib_executor: Executor | None = None, ) -> None: if body is not None and text is not None: @@ -846,13 +845,6 @@ async def _do_start_compression(self, coding: ContentCoding) -> None: executor=self._zlib_executor, ) assert self._body is not None - if self._zlib_executor_size is None and len(self._body) > LARGE_BODY_SIZE: - warnings.warn( - "Synchronous compression of large response bodies " - f"({len(self._body)} bytes) might block the async event loop. " - "Consider providing a custom value to zlib_executor_size/" - "zlib_executor response properties or disabling compression on it." - ) self._compressed_body = ( await compressor.compress(self._body) + compressor.flush() ) diff --git a/tests/test_benchmarks_client.py b/tests/test_benchmarks_client.py index 8e75ef9c040..3c362a500a7 100644 --- a/tests/test_benchmarks_client.py +++ b/tests/test_benchmarks_client.py @@ -504,6 +504,36 @@ def _run() -> None: loop.run_until_complete(run_client_benchmark()) +@pytest.mark.usefixtures("parametrize_zlib_backend") +def test_ten_compressed_responses_iter_chunked_1mb( + loop: asyncio.AbstractEventLoop, + aiohttp_client: AiohttpClient, + benchmark: BenchmarkFixture, +) -> None: + """Benchmark compressed GET request read via large iter_chunked.""" + MB = 2**20 + data = b"x" * 10 * MB + + async def handler(request: web.Request) -> web.Response: + resp = web.Response(body=data) + resp.enable_compression() + return resp + + app = web.Application() + app.router.add_route("GET", "/", handler) + + async def run_client_benchmark() -> None: + client = await aiohttp_client(app) + resp = await client.get("/") + async for _ in resp.content.iter_chunked(MB): + pass + await client.close() + + @benchmark + def _run() -> None: + loop.run_until_complete(run_client_benchmark()) + + def test_ten_streamed_responses_iter_chunks( loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient, diff --git a/tests/test_benchmarks_web_request.py b/tests/test_benchmarks_web_request.py new file mode 100644 index 00000000000..81afe7824e4 --- /dev/null +++ b/tests/test_benchmarks_web_request.py @@ -0,0 +1,42 @@ +"""codspeed benchmarks for web request reading.""" + +import asyncio +import zlib + +import pytest +from pytest_codspeed import BenchmarkFixture + +from aiohttp import web +from aiohttp.pytest_plugin import AiohttpClient + + +@pytest.mark.usefixtures("parametrize_zlib_backend") +def test_read_compressed_post_body( + loop: asyncio.AbstractEventLoop, + aiohttp_client: AiohttpClient, + benchmark: BenchmarkFixture, +) -> None: + """Benchmark server Request.read() with a compressed POST body.""" + original = b"B" * (5 * 2**20) + compressed = zlib.compress(original) + + async def handler(request: web.Request) -> web.Response: + body = await request.read() + return web.Response(text=str(len(body))) + + app = web.Application(client_max_size=10 * 2**20) + app.router.add_post("/", handler) + + async def run_benchmark() -> None: + client = await aiohttp_client(app) + resp = await client.post( + "/", + data=compressed, + headers={"Content-Encoding": "deflate"}, + ) + assert int(await resp.read()) == len(original) + await client.close() + + @benchmark + def _run() -> None: + loop.run_until_complete(run_benchmark()) diff --git a/tests/test_web_response.py b/tests/test_web_response.py index 799d34ce063..5299f1c8a3d 100644 --- a/tests/test_web_response.py +++ b/tests/test_web_response.py @@ -584,26 +584,6 @@ async def test_force_compression_deflate() -> None: assert "deflate" == resp.headers.get(hdrs.CONTENT_ENCODING) -@pytest.mark.usefixtures("parametrize_zlib_backend") -async def test_force_compression_deflate_large_payload() -> None: - """Make sure a warning is thrown for large payloads compressed in the event loop.""" - req = make_request( - "GET", "/", headers=CIMultiDict({hdrs.ACCEPT_ENCODING: "gzip, deflate"}) - ) - resp = Response(body=b"large") - - resp.enable_compression(ContentCoding.deflate) - assert resp.compression - - with ( - pytest.warns(Warning, match="Synchronous compression of large response bodies"), - mock.patch("aiohttp.web_response.LARGE_BODY_SIZE", 2), - ): - msg = await resp.prepare(req) - assert msg is not None - assert "deflate" == resp.headers.get(hdrs.CONTENT_ENCODING) - - @pytest.mark.usefixtures("parametrize_zlib_backend") async def test_force_compression_no_accept_deflate() -> None: req = make_request("GET", "/") From 3c56715def42e13f8783a585414e44337291b6f7 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sat, 18 Apr 2026 15:38:10 +0100 Subject: [PATCH 017/191] Optimise decompression size (#12357) (#12391) (cherry picked from commit 53f6e919dbae7b65627018f015014e0fe70b4d92) --- aiohttp/compression_utils.py | 10 ++++- aiohttp/http_parser.py | 9 +++-- aiohttp/streams.py | 63 +++++++++++++++++-------------- aiohttp/web_request.py | 4 ++ tests/test_flowcontrol_streams.py | 2 +- 5 files changed, 53 insertions(+), 35 deletions(-) diff --git a/aiohttp/compression_utils.py b/aiohttp/compression_utils.py index c836b6a3da4..a30894eb0b7 100644 --- a/aiohttp/compression_utils.py +++ b/aiohttp/compression_utils.py @@ -326,9 +326,15 @@ def decompress_sync( ) -> bytes: """Decompress the given data.""" if hasattr(self._obj, "decompress"): - result = cast(bytes, self._obj.decompress(data, max_length)) + if max_length == ZLIB_MAX_LENGTH_UNLIMITED: + result = cast(bytes, self._obj.decompress(data)) + else: + result = cast(bytes, self._obj.decompress(data, max_length)) else: - result = cast(bytes, self._obj.process(data, max_length)) + if max_length == ZLIB_MAX_LENGTH_UNLIMITED: + result = cast(bytes, self._obj.process(data)) + else: + result = cast(bytes, self._obj.process(data, max_length)) # Only way to know that brotli has no further data is checking we get no output self._last_empty = result == b"" return result diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py index f3ea42f6e3f..9e229cc4459 100644 --- a/aiohttp/http_parser.py +++ b/aiohttp/http_parser.py @@ -2,6 +2,7 @@ import asyncio import re import string +import sys from contextlib import suppress from enum import IntEnum from re import Pattern @@ -1130,10 +1131,12 @@ def feed_data(self, chunk: bytes, size: int) -> bool: encoding=self.encoding, suppress_deflate_header=True ) + low_water = self.out._low_water + max_length = ( + 0 if low_water >= sys.maxsize else max(self._max_decompress_size, low_water) + ) try: - chunk = self.decompressor.decompress_sync( - chunk, max_length=self._max_decompress_size - ) + chunk = self.decompressor.decompress_sync(chunk, max_length=max_length) except Exception: raise ContentEncodingError( "Can not decode content-encoding: %s" % self.encoding diff --git a/aiohttp/streams.py b/aiohttp/streams.py index 42b427ee0f0..196469c005a 100644 --- a/aiohttp/streams.py +++ b/aiohttp/streams.py @@ -1,5 +1,6 @@ import asyncio import collections +import sys import warnings from collections.abc import Awaitable, Callable from typing import Final, Generic, TypeVar @@ -67,31 +68,7 @@ async def __anext__(self) -> tuple[bytes, bool]: return rv -class AsyncStreamReaderMixin: - - __slots__ = () - - def __aiter__(self) -> AsyncStreamIterator[bytes]: - return AsyncStreamIterator(self.readline) # type: ignore[attr-defined] - - def iter_chunked(self, n: int) -> AsyncStreamIterator[bytes]: - """Returns an asynchronous iterator that yields chunks of size n.""" - return AsyncStreamIterator(lambda: self.read(n)) # type: ignore[attr-defined] - - def iter_any(self) -> AsyncStreamIterator[bytes]: - """Yield all available data as soon as it is received.""" - return AsyncStreamIterator(self.readany) # type: ignore[attr-defined] - - def iter_chunks(self) -> ChunkTupleAsyncStreamIterator: - """Yield chunks of data as they are received by the server. - - The yielded objects are tuples - of (bytes, bool) as returned by the StreamReader.readchunk method. - """ - return ChunkTupleAsyncStreamIterator(self) # type: ignore[arg-type] - - -class StreamReader(AsyncStreamReaderMixin): +class StreamReader: """An enhancement of asyncio.StreamReader. Supports asynchronous iteration by line, chunk or as available:: @@ -176,9 +153,35 @@ def __repr__(self) -> str: info.append("e=%r" % self._exception) return "<%s>" % " ".join(info) + def __aiter__(self) -> AsyncStreamIterator[bytes]: + return AsyncStreamIterator(self.readline) + + def iter_chunked(self, n: int) -> AsyncStreamIterator[bytes]: + """Returns an asynchronous iterator that yields chunks of size n.""" + self.set_read_chunk_size(n) + return AsyncStreamIterator(lambda: self.read(n)) + + def iter_any(self) -> AsyncStreamIterator[bytes]: + """Yield all available data as soon as it is received.""" + return AsyncStreamIterator(self.readany) + + def iter_chunks(self) -> ChunkTupleAsyncStreamIterator: + """Yield chunks of data as they are received by the server. + + The yielded objects are tuples + of (bytes, bool) as returned by the StreamReader.readchunk method. + """ + return ChunkTupleAsyncStreamIterator(self) + def get_read_buffer_limits(self) -> tuple[int, int]: return (self._low_water, self._high_water) + def set_read_chunk_size(self, n: int) -> None: + """Raise buffer limits to match the consumer's chunk size.""" + if n > self._low_water: + self._low_water = n + self._high_water = n * 2 + def exception(self) -> BaseException | None: return self._exception @@ -427,10 +430,8 @@ async def read(self, n: int = -1) -> bytes: return b"" if n < 0: - # This used to just loop creating a new waiter hoping to - # collect everything in self._buffer, but that would - # deadlock if the subprocess sends more than self.limit - # bytes. So just call self.readany() until EOF. + # Reading everything — remove decompression chunk limit. + self.set_read_chunk_size(sys.maxsize) blocks = [] while True: block = await self.readany() @@ -439,6 +440,7 @@ async def read(self, n: int = -1) -> bytes: blocks.append(block) return b"".join(blocks) + self.set_read_chunk_size(n) # TODO: should be `if` instead of `while` # because waiter maybe triggered on chunk end, # without feeding any data @@ -612,6 +614,9 @@ async def wait_eof(self) -> None: def feed_data(self, data: bytes, n: int = 0) -> bool: return False + def set_read_chunk_size(self, n: int) -> None: + return + async def readline(self, *, max_line_length: int | None = None) -> bytes: return b"" diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py index 47d50a12d5c..25d7ebc8e22 100644 --- a/aiohttp/web_request.py +++ b/aiohttp/web_request.py @@ -678,6 +678,10 @@ async def read(self) -> bytes: Returns bytes object with full request content. """ if self._read_bytes is None: + # Raise the buffer limits so compressed payloads decompress in + # larger chunks instead of many small pause/resume cycles. + if self._client_max_size: + self._payload.set_read_chunk_size(self._client_max_size) body = bytearray() while True: chunk = await self._payload.readany() diff --git a/tests/test_flowcontrol_streams.py b/tests/test_flowcontrol_streams.py index e71107b0e00..03b6eec44f5 100644 --- a/tests/test_flowcontrol_streams.py +++ b/tests/test_flowcontrol_streams.py @@ -82,7 +82,7 @@ async def test_readexactly(self, stream) -> None: stream.feed_data(b"data", 4) res = await stream.readexactly(3) assert res == b"dat" - assert not stream._protocol.resume_reading.called + assert stream._protocol.resume_reading.called async def test_feed_data(self, stream) -> None: stream._protocol._reading_paused = False From 6d81b02276d4caf21608ab16e3f15ef2eb4dde32 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:34:53 +0200 Subject: [PATCH 018/191] [PR #12394/24ed3b34 backport][3.14] docs: mention fake server testing pattern (#12396) **This is a backport of PR #12394 as merged into master (24ed3b343f4673b54b7b5a95bb441e1f7f93120b).** Fixes #980. Co-authored-by: nightcityblade --- docs/testing.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/testing.rst b/docs/testing.rst index a7b93e714f6..15437f38134 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -490,6 +490,12 @@ basis, the TestClient object can be used directly:: A full list of the utilities provided can be found at the :mod:`api reference ` +For end-to-end client code that talks to an external service, it is +recommended to run a small fake server rather than patching private aiohttp +internals. The ``examples/fake_server.py`` demo shows such an approach: start +a local :class:`~aiohttp.web.Application`, point a custom resolver at it, and +exercise the client against that controlled endpoint. + Testing API Reference --------------------- From 5d74702751484f849c41354ce0f501be5978bed0 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 15:27:40 +0100 Subject: [PATCH 019/191] [PR #12350/7eb05774 backport][3.13] Fix octal tests (#12401) **This is a backport of PR #12350 as merged into master (7eb057747aa8ac6b0031fd032b8e2bda53d98b53).** Co-authored-by: Sam Bull --- tests/test_cookie_helpers.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_cookie_helpers.py b/tests/test_cookie_helpers.py index 767e1eaa34a..fead869d6f3 100644 --- a/tests/test_cookie_helpers.py +++ b/tests/test_cookie_helpers.py @@ -1095,13 +1095,13 @@ def test_parse_set_cookie_headers_date_formats_with_attributes() -> None: @pytest.mark.parametrize( ("header", "expected_name", "expected_value", "expected_coded"), [ - # Test cookie values with octal escape sequences - (r'name="\012newline\012"', "name", "\nnewline\n", r'"\012newline\012"'), + # Test cookie values with octal escape sequences (printable chars only) + (r'name="\050parens\051"', "name", "(parens)", r'"\050parens\051"'), ( - r'tab="\011separated\011values"', - "tab", - "\tseparated\tvalues", - r'"\011separated\011values"', + r'punct="\053plus\053values"', + "punct", + "+plus+values", + r'"\053plus\053values"', ), ( r'mixed="hello\040world\041"', @@ -1110,10 +1110,10 @@ def test_parse_set_cookie_headers_date_formats_with_attributes() -> None: r'"hello\040world\041"', ), ( - r'complex="\042quoted\042 text with \012 newline"', + r'complex="\042quoted\042 text with \055 hyphen"', "complex", - '"quoted" text with \n newline', - r'"\042quoted\042 text with \012 newline"', + '"quoted" text with - hyphen', + r'"\042quoted\042 text with \055 hyphen"', ), ], ) From 4f5858566f30aa5b17c4d3eaed956d3455bfa5bb Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 20:02:56 +0100 Subject: [PATCH 020/191] [PR #12406/b0601d64 backport][3.14] Avoid installation of backports.zstd on Python 3.14 in linting deps (#12407) **This is a backport of PR #12406 as merged into master (b0601d64c65b1f7ac4823e3c6ef630bc4e0b8890).** Co-authored-by: Michael Seifert --- CHANGES/12406.contrib.rst | 2 ++ requirements/lint.in | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 CHANGES/12406.contrib.rst diff --git a/CHANGES/12406.contrib.rst b/CHANGES/12406.contrib.rst new file mode 100644 index 00000000000..9bcbee91e09 --- /dev/null +++ b/CHANGES/12406.contrib.rst @@ -0,0 +1,2 @@ +Avoid installation of backports.zstd on Python 3.14 in linting dependency set +-- by :user:`seifertm`. diff --git a/requirements/lint.in b/requirements/lint.in index 5bfd3c31c65..97ee198b9c9 100644 --- a/requirements/lint.in +++ b/requirements/lint.in @@ -1,5 +1,5 @@ aiodns -backports.zstd; implementation_name == "cpython" +backports.zstd; implementation_name == "cpython" and python_version < "3.14" blockbuster freezegun isal From d7193863355710592420f89cfcc3342861f957a7 Mon Sep 17 00:00:00 2001 From: jmestwa-coder Date: Mon, 4 May 2026 05:19:52 +0530 Subject: [PATCH 021/191] [3.14] Validate Content-Length format in ClientRequest (backport of #12385) (#12445) --- aiohttp/client_reqrep.py | 10 ++++------ tests/test_client_request.py | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index d8dd4971e4e..aa0d48159c0 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -87,6 +87,7 @@ _CONNECTION_CLOSED_EXCEPTION = ClientConnectionError("Connection closed") _CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]") json_re = re.compile(r"^application/(?:[\w.+-]+?\+)?json") +_DIGITS_RE = re.compile(r"\d+", re.ASCII) def _gen_default_accept_encoding() -> str: @@ -903,12 +904,9 @@ def _get_content_length(self) -> int | None: return None content_length_hdr = self.headers[hdrs.CONTENT_LENGTH] - try: - return int(content_length_hdr) - except ValueError: - raise ValueError( - f"Invalid Content-Length header: {content_length_hdr}" - ) from None + if not _DIGITS_RE.fullmatch(content_length_hdr): + raise ValueError(f"Invalid Content-Length header: {content_length_hdr!r}") + return int(content_length_hdr) @property def skip_auto_headers(self) -> CIMultiDict[None]: diff --git a/tests/test_client_request.py b/tests/test_client_request.py index 3d5898abc0d..5e23f085d45 100644 --- a/tests/test_client_request.py +++ b/tests/test_client_request.py @@ -1667,7 +1667,24 @@ def test_get_content_length(make_request: _RequestMaker) -> None: # Invalid Content-Length header req.headers["Content-Length"] = "invalid" - with pytest.raises(ValueError, match="Invalid Content-Length header: invalid"): + with pytest.raises(ValueError, match="Invalid Content-Length header"): + req._get_content_length() + + +async def test_get_content_length_invalid_formats( + make_request: _RequestMaker, +) -> None: + req = make_request("GET", URL("http://python.org/")) + + req.headers["Content-Length"] = "100" + assert req._get_content_length() == 100 + + req.headers["Content-Length"] = "-100" + with pytest.raises(ValueError, match="Invalid Content-Length header"): + req._get_content_length() + + req.headers["Content-Length"] = "५" # Devengali number 5 + with pytest.raises(ValueError, match="Invalid Content-Length header"): req._get_content_length() From 422566982274d0b5b8679c8f2c5d28630c2b803b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 00:57:51 +0000 Subject: [PATCH 022/191] Bump softprops/action-gh-release from 2 to 3 (#12362) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3.
Release notes

Sourced from softprops/action-gh-release's releases.

v3.0.0

3.0.0 is a major release that moves the action runtime from Node 20 to Node 24. Use v3 on GitHub-hosted runners and self-hosted fleets that already support the Node 24 Actions runtime. If you still need the last Node 20-compatible line, stay on v2.6.2.

What's Changed

Other Changes 🔄

  • Move the action runtime and bundle target to Node 24
  • Update @types/node to the Node 24 line and allow future Dependabot updates
  • Keep the floating major tag on v3; v2 remains pinned to the latest 2.x release

v2.6.2

What's Changed

Other Changes 🔄

Full Changelog: https://github.com/softprops/action-gh-release/compare/v2...v2.6.2

v2.6.1

2.6.1 is a patch release focused on restoring linked discussion thread creation when discussion_category_name is set. It fixes [#764](https://github.com/softprops/action-gh-release/issues/764), where the draft-first publish flow stopped carrying the discussion category through the final publish step.

If you still hit an issue after upgrading, please open a report with the bug template and include a minimal repro or sanitized workflow snippet where possible.

What's Changed

Bug fixes 🐛

v2.6.0

2.6.0 is a minor release centered on previous_tag support for generate_release_notes, which lets workflows pin GitHub's comparison base explicitly instead of relying on the default range. It also includes the recent concurrent asset upload recovery fix, a working_directory docs sync, a checked-bundle freshness guard for maintainers, and clearer immutable-prerelease guidance where GitHub platform behavior imposes constraints on how prerelease asset uploads can be published.

If you still hit an issue after upgrading, please open a report with the bug template and include a minimal repro or sanitized workflow snippet where possible.

What's Changed

... (truncated)

Changelog

Sourced from softprops/action-gh-release's changelog.

0.1.13

  • fix issue with multiple runs concatenating release bodies #145
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=softprops/action-gh-release&package-manager=github_actions&previous-version=2&new-version=3)](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> Co-authored-by: Sam Bull --- .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 f4c6b972cf7..9573decd95e 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -692,7 +692,7 @@ jobs: # Confusingly, this action also supports updating releases, not # just creating them. This is what we want here, since we've manually # created the release above. - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: # dist/ contains the built packages, which smoketest-artifacts/ # contains the signatures and certificates. From 299693b485b98b9a14ee4ddf57a1b10b59fdc0f9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 02:37:14 +0100 Subject: [PATCH 023/191] Bump actions/upload-artifact from 6 to 7 (#12156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
Release notes

Sourced from actions/upload-artifact's releases.

v7.0.0

v7 What's new

Direct Uploads

Adds support for uploading single files directly (unzipped). Callers can set the new archive parameter to false to skip zipping the file during upload. Right now, we only support single files. The action will fail if the glob passed resolves to multiple files. The name parameter is also ignored with this setting. Instead, the name of the artifact will be the name of the uploaded file.

ESM

To support new versions of the @actions/* packages, we've upgraded the package to ESM.

What's Changed

New Contributors

Full Changelog: https://github.com/actions/upload-artifact/compare/v6...v7.0.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=6&new-version=7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR 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)
> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-cd.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 9573decd95e..52b4fac1e02 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -135,7 +135,7 @@ jobs: run: | make generate-llhttp - name: Upload llhttp generated files - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: llhttp path: vendor/llhttp/build @@ -527,7 +527,7 @@ jobs: run: | python -m build --sdist - name: Upload artifacts - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: dist-sdist path: dist @@ -619,7 +619,7 @@ jobs: CIBW_SKIP: pp* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }} CIBW_ARCHS_MACOS: x86_64 arm64 universal2 - name: Upload wheels - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: >- dist-${{ matrix.os }}-${{ matrix.musl }}-${{ From c2fdd7395f37fd81161dff5936d26533a674808e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 02:45:29 +0000 Subject: [PATCH 024/191] Bump cryptography from 46.0.4 to 46.0.5 (#12056) Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.4 to 46.0.5.
Changelog

Sourced from cryptography's changelog.

46.0.5 - 2026-02-10


* An attacker could create a malicious public key that reveals portions
of your
private key when using certain uncommon elliptic curves (binary curves).
This version now includes additional security checks to prevent this
attack.
This issue only affects binary elliptic curves, which are rarely used in
real-world applications. Credit to **XlabAI Team of Tencent Xuanwu Lab
and
Atuin Automated Vulnerability Discovery Engine** for reporting the
issue.
  **CVE-2026-26007**
* Support for ``SECT*`` binary elliptic curves is deprecated and will be
  removed in the next release.

.. v46-0-4:

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cryptography&package-manager=pip&previous-version=46.0.4&new-version=46.0.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR 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)
> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sam Bull --- 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 0bc7d27d78f..5cc3b3d7b58 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -59,7 +59,7 @@ coverage==7.13.4 # via # -r requirements/test-common.in # pytest-cov -cryptography==46.0.4 +cryptography==46.0.5 # via trustme cython==3.2.4 # via -r requirements/cython.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 7d67f91b6f2..14bbbc746dd 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -59,7 +59,7 @@ coverage==7.13.4 # via # -r requirements/test-common.in # pytest-cov -cryptography==46.0.4 +cryptography==46.0.5 # via trustme distlib==0.4.0 # via virtualenv diff --git a/requirements/lint.txt b/requirements/lint.txt index 9cea924d820..1db16f4ccd1 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -23,7 +23,7 @@ cfgv==3.5.0 # via pre-commit click==8.3.1 # via slotscheck -cryptography==46.0.4 +cryptography==46.0.5 # via trustme distlib==0.4.0 # via virtualenv diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 93ee412a37c..c34601a5ebd 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -18,7 +18,7 @@ coverage==7.13.4 # via # -r requirements/test-common.in # pytest-cov -cryptography==46.0.4 +cryptography==46.0.5 # via trustme exceptiongroup==1.3.1 # via pytest diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 115d8891785..10e66451c36 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -33,7 +33,7 @@ coverage==7.13.4 # via # -r requirements/test-common.in # pytest-cov -cryptography==46.0.4 +cryptography==46.0.5 # via trustme exceptiongroup==1.3.1 # via pytest diff --git a/requirements/test.txt b/requirements/test.txt index f9603ae4a09..6fa162e1ea7 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -33,7 +33,7 @@ coverage==7.13.4 # via # -r requirements/test-common.in # pytest-cov -cryptography==46.0.4 +cryptography==46.0.5 # via trustme exceptiongroup==1.3.1 # via pytest From f1dfd44d8be450cba8e85e82cb4df80dd280ec32 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 02:46:17 +0000 Subject: [PATCH 025/191] Bump imagesize from 1.4.1 to 1.5.0 (#12191) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [imagesize](https://github.com/shibukawa/imagesize_py) from 1.4.1 to 1.5.0.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=imagesize&package-manager=pip&previous-version=1.4.1&new-version=1.5.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR 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)
> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sam Bull --- 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 5cc3b3d7b58..a807f9cae83 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -94,7 +94,7 @@ idna==3.11 # requests # trustme # yarl -imagesize==1.4.1 +imagesize==1.5.0 # via sphinx iniconfig==2.3.0 # via pytest diff --git a/requirements/dev.txt b/requirements/dev.txt index 14bbbc746dd..1184c9144fa 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -92,7 +92,7 @@ idna==3.11 # requests # trustme # yarl -imagesize==1.4.1 +imagesize==1.5.0 # via sphinx iniconfig==2.3.0 # via pytest diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index de53158c046..aef9eb6fedc 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -20,7 +20,7 @@ docutils==0.21.2 # via sphinx idna==3.11 # via requests -imagesize==1.4.1 +imagesize==1.5.0 # via sphinx jinja2==3.1.6 # via diff --git a/requirements/doc.txt b/requirements/doc.txt index b2b5697d659..c3214743726 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -20,7 +20,7 @@ docutils==0.21.2 # via sphinx idna==3.11 # via requests -imagesize==1.4.1 +imagesize==1.5.0 # via sphinx jinja2==3.1.6 # via From 09d2f03db1ed82033e3b1ca4c7686301da44d7c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 02:51:57 +0000 Subject: [PATCH 026/191] Bump actions/github-script from 8 to 9 (#12348) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/github-script](https://github.com/actions/github-script) from 8 to 9.
Release notes

Sourced from actions/github-script's releases.

v9.0.0

New features:

  • getOctokit factory function — Available directly in the script context. Create additional authenticated Octokit clients with different tokens for multi-token workflows, GitHub App tokens, and cross-org access. See Creating additional clients with getOctokit for details and examples.
  • Orchestration ID in user-agent — The ACTIONS_ORCHESTRATION_ID environment variable is automatically appended to the user-agent string for request tracing.

Breaking changes:

  • require('@actions/github') no longer works in scripts. The upgrade to @actions/github v9 (ESM-only) means require('@actions/github') will fail at runtime. If you previously used patterns like const { getOctokit } = require('@actions/github') to create secondary clients, use the new injected getOctokit function instead — it's available directly in the script context with no imports needed.
  • getOctokit is now an injected function parameter. Scripts that declare const getOctokit = ... or let getOctokit = ... will get a SyntaxError because JavaScript does not allow const/let redeclaration of function parameters. Use the injected getOctokit directly, or use var getOctokit = ... if you need to redeclare it.
  • If your script accesses other @actions/github internals beyond the standard github/octokit client, you may need to update those references for v9 compatibility.

What's Changed

New Contributors

Full Changelog: https://github.com/actions/github-script/compare/v8.0.0...v9.0.0

Commits
  • 3a2844b Merge pull request #700 from actions/salmanmkc/expose-getoctokit + prepare re...
  • ca10bbd fix: use @​octokit/core/types import for v7 compatibility
  • 86e48e2 merge: incorporate main branch changes
  • c108472 chore: rebuild dist for v9 upgrade and getOctokit factory
  • afff112 Merge pull request #712 from actions/salmanmkc/deployment-false + fix user-ag...
  • ff8117e ci: fix user-agent test to handle orchestration ID
  • 81c6b78 ci: use deployment: false to suppress deployment noise from integration tests
  • 3953caf docs: update README examples from @​v8 to @​v9, add getOctokit docs and v9 brea...
  • c17d55b ci: add getOctokit integration test job
  • a047196 test: add getOctokit integration tests via callAsyncFunction
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/github-script&package-manager=github_actions&previous-version=8&new-version=9)](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> Co-authored-by: Sam Bull --- .github/workflows/labels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index e3f10214082..cb8e99f572d 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -11,7 +11,7 @@ jobs: name: Backport label added if: ${{ github.event.pull_request.user.type != 'Bot' }} steps: - - uses: actions/github-script@v8 + - uses: actions/github-script@v9 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | From 65bd5bcbbee14c67fdd86a1714a841c8f186130e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 02:53:34 +0000 Subject: [PATCH 027/191] Bump multidict from 6.7.0 to 6.7.1 (#12001) Bumps [multidict](https://github.com/aio-libs/multidict) from 6.7.0 to 6.7.1.
Release notes

Sourced from multidict's releases.

6.7.1

Bug fixes

  • Fixed slow memory leak caused by identity by adding Py_DECREF to identity value before leaving md_pop_one on success -- by :user:Vizonex.

    Related issues and pull requests on GitHub: #1284.


Changelog

Sourced from multidict's changelog.

6.7.1

(2026-01-25)

Bug fixes

  • Fixed slow memory leak caused by identity by adding Py_DECREF to identity value before leaving md_pop_one on success -- by :user:Vizonex.

    Related issues and pull requests on GitHub: :issue:1284.


Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=multidict&package-manager=pip&previous-version=6.7.0&new-version=6.7.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR 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)
> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sam Bull --- requirements/base-ft.txt | 2 +- requirements/base.txt | 2 +- requirements/constraints.txt | 2 +- requirements/cython.txt | 2 +- requirements/dev.txt | 2 +- requirements/multidict.txt | 2 +- requirements/runtime-deps.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt index 1048f618136..3598283c782 100644 --- a/requirements/base-ft.txt +++ b/requirements/base-ft.txt @@ -28,7 +28,7 @@ gunicorn==25.1.0 # via -r requirements/base-ft.in idna==3.11 # via yarl -multidict==6.7.0 +multidict==6.7.1 # via # -r requirements/runtime-deps.in # yarl diff --git a/requirements/base.txt b/requirements/base.txt index 4c8df4dd20a..f27766b8194 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -28,7 +28,7 @@ gunicorn==25.1.0 # via -r requirements/base.in idna==3.11 # via yarl -multidict==6.7.0 +multidict==6.7.1 # via # -r requirements/runtime-deps.in # yarl diff --git a/requirements/constraints.txt b/requirements/constraints.txt index a807f9cae83..c2d5184bb24 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -114,7 +114,7 @@ markupsafe==3.0.3 # via jinja2 mdurl==0.1.2 # via markdown-it-py -multidict==6.7.0 +multidict==6.7.1 # via # -r requirements/multidict.in # -r requirements/runtime-deps.in diff --git a/requirements/cython.txt b/requirements/cython.txt index 03fa291992c..193a2299871 100644 --- a/requirements/cython.txt +++ b/requirements/cython.txt @@ -6,7 +6,7 @@ # cython==3.2.4 # via -r requirements/cython.in -multidict==6.7.0 +multidict==6.7.1 # via -r requirements/multidict.in typing-extensions==4.15.0 # via multidict diff --git a/requirements/dev.txt b/requirements/dev.txt index 1184c9144fa..76e048db44f 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -112,7 +112,7 @@ markupsafe==3.0.3 # via jinja2 mdurl==0.1.2 # via markdown-it-py -multidict==6.7.0 +multidict==6.7.1 # via # -r requirements/runtime-deps.in # yarl diff --git a/requirements/multidict.txt b/requirements/multidict.txt index 39d1c2a3c8e..144a7966d26 100644 --- a/requirements/multidict.txt +++ b/requirements/multidict.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/multidict.txt --resolver=backtracking --strip-extras requirements/multidict.in # -multidict==6.7.0 +multidict==6.7.1 # via -r requirements/multidict.in typing-extensions==4.15.0 # via multidict diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index 6d38a801a10..f3a3df711ab 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -26,7 +26,7 @@ frozenlist==1.8.0 # aiosignal idna==3.11 # via yarl -multidict==6.7.0 +multidict==6.7.1 # via # -r requirements/runtime-deps.in # yarl diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 10e66451c36..b61eec5f04a 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -63,7 +63,7 @@ markdown-it-py==4.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -multidict==6.7.0 +multidict==6.7.1 # via # -r requirements/runtime-deps.in # yarl diff --git a/requirements/test.txt b/requirements/test.txt index 6fa162e1ea7..62cc042c3ba 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -63,7 +63,7 @@ markdown-it-py==4.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -multidict==6.7.0 +multidict==6.7.1 # via # -r requirements/runtime-deps.in # yarl From ae0bdcc34b8b58792d54b6199ee2da9c91d6717b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 13:18:56 +0000 Subject: [PATCH 028/191] Bump dependabot/fetch-metadata from 2 to 3 (#12344) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 2 to 3.
Release notes

Sourced from dependabot/fetch-metadata's releases.

v3.0.0

The breaking change is requiring Node.js version v24 as the Actions runtime.

What's Changed

New Contributors

Full Changelog: https://github.com/dependabot/fetch-metadata/compare/v2...v3.0.0

v2.5.0

What's Changed

... (truncated)

Commits
  • ffa630c v3.0.0 (#686)
  • ec8fff2 Merge pull request #674 from dependabot/dependabot/npm_and_yarn/picomatch-2.3.2
  • caf48bd build(deps-dev): bump picomatch from 2.3.1 to 2.3.2
  • 13d8274 Upgrade @​actions/github to ^9.0.0 and @​octokit/request-error to ^7.1.0 (#678)
  • b603099 Upgrade @​actions/core from ^1.11.1 to ^3.0.0 (#677)
  • c5dc5b1 Enable noImplicitAny in tsconfig.json (#684)
  • a183f3c Add typecheck step to CI (#685)
  • 5e17564 Remove skipLibCheck from tsconfig.json (#683)
  • bb56eeb Switch tsconfig module resolution to bundler (#682)
  • 3632e3d Remove vestigial outDir from tsconfig.json (#681)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dependabot/fetch-metadata&package-manager=github_actions&previous-version=2&new-version=3)](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> Co-authored-by: Sam Bull --- .github/workflows/auto-merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml index 56575750fe1..aa1b7c6890c 100644 --- a/.github/workflows/auto-merge.yml +++ b/.github/workflows/auto-merge.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v2 + uses: dependabot/fetch-metadata@v3 with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Enable auto-merge for Dependabot PRs From e23e25e1b05d5cc87da61c19056704a478d8b894 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 13:53:28 +0000 Subject: [PATCH 029/191] Bump librt from 0.8.1 to 0.9.0 (#12450) Bumps [librt](https://github.com/mypyc/librt) from 0.8.1 to 0.9.0.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=librt&package-manager=pip&previous-version=0.8.1&new-version=0.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 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 | 2 +- requirements/lint.txt | 4 ++-- requirements/test-common.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index c2d5184bb24..3d47fe07dde 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -26,7 +26,7 @@ attrs==25.4.0 # via -r requirements/runtime-deps.in babel==2.18.0 # via sphinx -backports-zstd==1.3.0 ; implementation_name == "cpython" +backports-zstd==1.3.0 ; implementation_name == "cpython" and python_version < "3.14" # via # -r requirements/lint.in # -r requirements/runtime-deps.in @@ -106,7 +106,7 @@ jinja2==3.1.6 # via # sphinx # towncrier -librt==0.8.1 +librt==0.9.0 # via mypy markdown-it-py==4.0.0 # via rich diff --git a/requirements/dev.txt b/requirements/dev.txt index 76e048db44f..e021873638c 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -104,7 +104,7 @@ jinja2==3.1.6 # via # sphinx # towncrier -librt==0.8.1 +librt==0.9.0 # via mypy markdown-it-py==4.0.0 # via rich diff --git a/requirements/lint.txt b/requirements/lint.txt index 1db16f4ccd1..77209a1d445 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -10,7 +10,7 @@ annotated-types==0.7.0 # via pydantic async-timeout==5.0.1 # via valkey -backports-zstd==1.3.0 ; implementation_name == "cpython" +backports-zstd==1.3.0 ; implementation_name == "cpython" and python_version < "3.14" # via -r requirements/lint.in blockbuster==1.5.26 # via -r requirements/lint.in @@ -45,7 +45,7 @@ iniconfig==2.3.0 # via pytest isal==1.7.2 # via -r requirements/lint.in -librt==0.8.1 +librt==0.9.0 # via mypy markdown-it-py==4.0.0 # via rich diff --git a/requirements/test-common.txt b/requirements/test-common.txt index c34601a5ebd..35bb72c8c73 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -34,7 +34,7 @@ iniconfig==2.3.0 # via pytest isal==1.8.0 ; python_version < "3.14" # via -r requirements/test-common.in -librt==0.8.1 +librt==0.9.0 # via mypy markdown-it-py==4.0.0 # via rich diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index b61eec5f04a..f38961394dc 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -57,7 +57,7 @@ iniconfig==2.3.0 # via pytest isal==1.8.0 ; python_version < "3.14" # via -r requirements/test-common.in -librt==0.8.1 +librt==0.9.0 # via mypy markdown-it-py==4.0.0 # via rich diff --git a/requirements/test.txt b/requirements/test.txt index 62cc042c3ba..9a1a0470ef0 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -57,7 +57,7 @@ iniconfig==2.3.0 # via pytest isal==1.7.2 ; python_version < "3.14" # via -r requirements/test-common.in -librt==0.8.1 +librt==0.9.0 # via mypy markdown-it-py==4.0.0 # via rich From e2d6ac0baeb8280092593d9e8e8b73b2538cd41c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 14:47:57 +0000 Subject: [PATCH 030/191] Bump regex from 2026.2.19 to 2026.2.28 (#12177) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [regex](https://github.com/mrabarnett/mrab-regex) from 2026.2.19 to 2026.2.28.
Changelog

Sourced from regex's changelog.

Version: 2026.2.28

Replaced atomic operations with mutex on pattern object for
free-threaded Python.

Version: 2026.2.26

PR
[#598](https://github.com/mrabarnett/mrab-regex/issues/598): Fix race
condition in storage caching with atomic operations.

Replaced use of PyUnicode_GET_LENGTH with PyUnicode_GetLength.

Version: 2026.2.19

Added \z as alias of \Z, like in re module.

Added prefixmatch as alias of match, like in re module.

Version: 2026.1.15

Re-uploaded.

Version: 2026.1.14

Git issue 596: Specifying {e<=0} causes ca 210× slow-down.

Added RISC-V wheels.

Version: 2025.11.3

Git issue 594: Support relative PARNO in recursive
subpatterns.

Version: 2025.10.23

'setup.py' was missing from the source distribution.

Version: 2025.10.22

Fixed test in main.yml.

Version: 2025.10.21

Moved tests into subfolder.

Version: 2025.10.20

Re-organised files.

Updated to Unicode 17.0.0.

Version: 2025.9.20

... (truncated)

Commits
  • df2d5ac Replaced atomic operations with mutex on pattern object for free-threaded Pyt...
  • ed3d9ca Replaced use of PyUnicode_GET_LENGTH with PyUnicode_GetLength.
  • 28dd3e7 Merge pull request #598 from kevmo314/fix-storage-caching-race
  • cd631d8 Fix race condition in storage caching with atomic operations
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=regex&package-manager=pip&previous-version=2026.2.19&new-version=2026.2.28)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR 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)
> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sam Bull --- requirements/constraints.txt | 2 +- requirements/dev.txt | 2 +- requirements/test-common.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 3d47fe07dde..b66ab9a7524 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -207,7 +207,7 @@ pyyaml==6.0.3 # via pre-commit re-assert==1.1.0 # via -r requirements/test-common.in -regex==2026.2.19 +regex==2026.2.28 # via re-assert requests==2.32.5 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index e021873638c..3ea0c9d728c 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -202,7 +202,7 @@ pyyaml==6.0.3 # via pre-commit re-assert==1.1.0 # via -r requirements/test-common.in -regex==2026.2.19 +regex==2026.2.28 # via re-assert requests==2.32.5 # via sphinx diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 35bb72c8c73..878bd428141 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -87,7 +87,7 @@ python-on-whales==0.80.0 # via -r requirements/test-common.in re-assert==1.1.0 # via -r requirements/test-common.in -regex==2026.2.19 +regex==2026.2.28 # via re-assert rich==14.3.3 # via pytest-codspeed diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index f38961394dc..e83b5d1bb7f 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -122,7 +122,7 @@ python-on-whales==0.80.0 # via -r requirements/test-common.in re-assert==1.1.0 # via -r requirements/test-common.in -regex==2026.2.19 +regex==2026.2.28 # via re-assert rich==14.3.3 # via pytest-codspeed diff --git a/requirements/test.txt b/requirements/test.txt index 9a1a0470ef0..8b8e81854f0 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -122,7 +122,7 @@ python-on-whales==0.80.0 # via -r requirements/test-common.in re-assert==1.1.0 # via -r requirements/test-common.in -regex==2026.2.19 +regex==2026.2.28 # via re-assert rich==14.3.3 # via pytest-codspeed From 54dc142f656d9c448ad0ebd835633a586a7ab3fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 14:59:57 +0000 Subject: [PATCH 031/191] Bump build from 1.3.0 to 1.4.0 (#11942) Bumps [build](https://github.com/pypa/build) from 1.3.0 to 1.4.0.
Release notes

Sourced from build's releases.

1.4.0

  • Add --quiet flag (PR #947)
  • Add option to dump PEP 517 metadata with --metadata (PR #940, PR #943)
  • Support UV environment variable (PR #971)
  • Remove a workaround for 3.14b1 (PR #960)
  • In 3.14 final release, color defaults to True already (PR #962)
  • Pass sp-repo-review (PR #942)
  • In pytest configuration, log_level is better than log_cli_level (PR #950)
  • Split up typing and mypy (PR #944)
  • Use types-colorama (PR #945)
  • In docs, first argument for _has_dependency is a name (PR #970)
  • Fix test failure when flit-core is installed (PR #921)
Changelog

Sourced from build's changelog.

1.4.0 (2026-01-08)

  • Add --quiet flag (:pr:947)
  • Add option to dump PEP 517 metadata with --metadata (:pr:940, :pr:943)
  • Support UV environment variable (:pr:971)
  • Remove a workaround for 3.14b1 (:pr:960)
  • In 3.14 final release, color defaults to True already (:pr:962)
  • Pass sp-repo-review (:pr:942)
  • In pytest configuration, log_level is better than log_cli_level (:pr:950)
  • Split up typing and mypy (:pr:944)
  • Use types-colorama (:pr:945)
  • In docs, first argument for _has_dependency is a name (PR :pr:970)
  • Fix test failure when flit-core is installed (PR :pr:921)
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=build&package-manager=pip&previous-version=1.3.0&new-version=1.4.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR 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)
> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sam Bull --- requirements/constraints.txt | 2 +- requirements/dev.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index b66ab9a7524..f807f2aad11 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -36,7 +36,7 @@ blockbuster==1.5.26 # -r requirements/test-common.in brotli==1.2.0 ; platform_python_implementation == "CPython" # via -r requirements/runtime-deps.in -build==1.3.0 +build==1.4.0 # via pip-tools certifi==2026.2.25 # via requests diff --git a/requirements/dev.txt b/requirements/dev.txt index 3ea0c9d728c..78c5f3620f8 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -36,7 +36,7 @@ blockbuster==1.5.26 # -r requirements/test-common.in brotli==1.2.0 ; platform_python_implementation == "CPython" # via -r requirements/runtime-deps.in -build==1.3.0 +build==1.4.0 # via pip-tools certifi==2026.2.25 # via requests From 299ffe965315d3e5b03dd53a3dd4c9067e2e626f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 15:09:46 +0000 Subject: [PATCH 032/191] Bump packaging from 26.0 to 26.2 (#12449) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [packaging](https://github.com/pypa/packaging) from 26.0 to 26.2.
Release notes

Sourced from packaging's releases.

26.2

What's Changed

Fixes:

Documentation:

Internal:

New Contributors

Full Changelog: https://github.com/pypa/packaging/compare/26.1...26.2

26.1

Features:

Behavior adaptations:

... (truncated)

Changelog

Sourced from packaging's changelog.

26.2 - 2026-04-24


Fixes:
  • Fix incorrect sysconfig var name for pyemscripten in (:pull:1160)
  • Make Version, Specifier, SpecifierSet, Tag, Marker, and Requirement pickle-safe
    and backward-compatible with pickles created in 25.0-26.1 (including references to the removed
    packaging._structures module) (:pull:1163, :pull:1168, :pull:1170, :pull:1171)
  • Re-export ExceptionGroup in metadata for now in (:pull:1164)

Documentation:

  • Add errors section and fix missing details in (:pull:1159)
  • Document our property-based test suite in (:pull:1167)
  • Fix a DirectUrl typo in (:pull:1167)
  • Add example of is_unsatisfiable in (:pull:1166)

Internal:

  • Enable the auditor persona on zizmor in (:pull:1158)
  • Test new pickle guarantees in (:pull:1174)
  • Use new native ReadTheDocs uv integration in (:pull:1175)

26.1 - 2026-04-14

Features:

  • PEP 783: add handling for Emscripten wheel tags in (:pull:804) (old name used in implementation, fixed in next release)
  • PEP 803: add handling for the abi3.abi3t free-threading tag in (:pull:1099)
  • PEP 723: add packaging.dependency_groups module, based on the dependency-groups package in (:pull:1065)
  • Add the packaging.direct_url module in (:pull:944)
  • Add the packaging.errors module in (:pull:1071)
  • Add SpecifierSet.is_unsatisfiable using ranges (new internals that will be expanded in future versions) in (:pull:1119)
  • Add create_compatible_tags_selector to select compatible tags in (:pull:1110)
  • Add a key argument to SpecifierSet.filter() in (:pull:1068)
  • Support & and | for Marker's in (:pull:1146)
  • Normalize Version.__replace__ and add Version.from_parts in (:pull:1078)
  • Add an option to validate compressed tag set sort order in parse_wheel_filename in (:pull:1150)

Behavior adaptations:

  • Narrow exclusion of pre-releases for <V.postN to match spec in (:pull:1140)
  • Narrow exclusion of post-releases for >V to match spec in (:pull:1141)
  • Rename format_full_version to _format_full_version to make it visibly private in (:pull:1125)
  • Restrict local version to ASCII in (:pull:1102)

Pylock (PEP 751) updates:

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=packaging&package-manager=pip&previous-version=26.0&new-version=26.2)](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/base-ft.txt | 2 +- requirements/base.txt | 2 +- requirements/constraints.txt | 2 +- requirements/dev.txt | 2 +- requirements/doc-spelling.txt | 2 +- requirements/doc.txt | 2 +- requirements/lint.txt | 2 +- requirements/test-common.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt index 3598283c782..cc91c67fd67 100644 --- a/requirements/base-ft.txt +++ b/requirements/base-ft.txt @@ -32,7 +32,7 @@ multidict==6.7.1 # via # -r requirements/runtime-deps.in # yarl -packaging==26.0 +packaging==26.2 # via gunicorn propcache==0.4.1 # via diff --git a/requirements/base.txt b/requirements/base.txt index f27766b8194..2eb77bc03f8 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -32,7 +32,7 @@ multidict==6.7.1 # via # -r requirements/runtime-deps.in # yarl -packaging==26.0 +packaging==26.2 # via gunicorn propcache==0.4.1 # via diff --git a/requirements/constraints.txt b/requirements/constraints.txt index f807f2aad11..3c8e567785b 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -127,7 +127,7 @@ mypy-extensions==1.1.0 # via mypy nodeenv==1.10.0 # via pre-commit -packaging==26.0 +packaging==26.2 # via # build # gunicorn diff --git a/requirements/dev.txt b/requirements/dev.txt index 78c5f3620f8..bb588cdcae6 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -124,7 +124,7 @@ mypy-extensions==1.1.0 # via mypy nodeenv==1.10.0 # via pre-commit -packaging==26.0 +packaging==26.2 # via # build # gunicorn diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index aef9eb6fedc..54885637357 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -28,7 +28,7 @@ jinja2==3.1.6 # towncrier markupsafe==3.0.3 # via jinja2 -packaging==26.0 +packaging==26.2 # via sphinx pyenchant==3.3.0 # via sphinxcontrib-spelling diff --git a/requirements/doc.txt b/requirements/doc.txt index c3214743726..2fb865888b4 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -28,7 +28,7 @@ jinja2==3.1.6 # towncrier markupsafe==3.0.3 # via jinja2 -packaging==26.0 +packaging==26.2 # via sphinx pygments==2.19.2 # via sphinx diff --git a/requirements/lint.txt b/requirements/lint.txt index 77209a1d445..3026c475c36 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -57,7 +57,7 @@ mypy-extensions==1.1.0 # via mypy nodeenv==1.10.0 # via pre-commit -packaging==26.0 +packaging==26.2 # via pytest pathspec==1.0.4 # via mypy diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 878bd428141..2068e9166c6 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -44,7 +44,7 @@ mypy==1.19.1 ; implementation_name == "cpython" # via -r requirements/test-common.in mypy-extensions==1.1.0 # via mypy -packaging==26.0 +packaging==26.2 # via pytest pathspec==1.0.4 # via mypy diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index e83b5d1bb7f..c8495179b3e 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -71,7 +71,7 @@ mypy==1.19.1 ; implementation_name == "cpython" # via -r requirements/test-common.in mypy-extensions==1.1.0 # via mypy -packaging==26.0 +packaging==26.2 # via # gunicorn # pytest diff --git a/requirements/test.txt b/requirements/test.txt index 8b8e81854f0..f119eaa25b5 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -71,7 +71,7 @@ mypy==1.19.1 ; implementation_name == "cpython" # via -r requirements/test-common.in mypy-extensions==1.1.0 # via mypy -packaging==26.0 +packaging==26.2 # via # gunicorn # pytest From 9506e32f337048ceab5a50dd7fe1dcd858c41641 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 15:12:24 +0000 Subject: [PATCH 033/191] Bump idna from 3.11 to 3.13 (#12451) Bumps [idna](https://github.com/kjd/idna) from 3.11 to 3.13.
Changelog

Sourced from idna's changelog.

3.13 (2026-04-22) +++++++++++++++++

  • Correct classification error for codepoint U+A7F1

3.12 (2026-04-21) +++++++++++++++++

  • Update to Unicode 17.0.0.
  • Issue a deprecation warning for the transitional argument.
  • Added lazy-loading to provide some performance improvements.
  • Removed vestiges of code related to Python 2 support, including segmentation of data structures specific to Jython.

Thanks to Rodrigo Nogueira for contributions to this release.

Commits
  • 89cdfd2 Release v3.13
  • 1eb0686 Pre-release 3.13
  • 5f20d1e Merge pull request #220 from kjd/unicode-next
  • 4ea8425 Regenerate idnadata.py with correct NFKC_CF data
  • fd47341 Use NFKC_CF from Unicode data files instead of Python's unicodedata module
  • a5304a4 Merge pull request #219 from kjd/release-3.12
  • d80d6f9 Release v3.12
  • 1bb44dd Merge pull request #218 from kjd/release-candidate-3.12rc0
  • 909c49d Release candidate for 3.12
  • c5459a1 Merge pull request #217 from kjd/housekeeping-2
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=idna&package-manager=pip&previous-version=3.11&new-version=3.13)](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/base-ft.txt | 2 +- requirements/base.txt | 2 +- requirements/constraints.txt | 2 +- requirements/dev.txt | 2 +- requirements/doc-spelling.txt | 2 +- requirements/doc.txt | 2 +- requirements/lint.txt | 2 +- requirements/runtime-deps.txt | 2 +- requirements/test-common.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt index cc91c67fd67..533c50b5f88 100644 --- a/requirements/base-ft.txt +++ b/requirements/base-ft.txt @@ -26,7 +26,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==25.1.0 # via -r requirements/base-ft.in -idna==3.11 +idna==3.13 # via yarl multidict==6.7.1 # via diff --git a/requirements/base.txt b/requirements/base.txt index 2eb77bc03f8..abc83087f3d 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -26,7 +26,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==25.1.0 # via -r requirements/base.in -idna==3.11 +idna==3.13 # via yarl multidict==6.7.1 # via diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 3c8e567785b..54b69c1677b 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -89,7 +89,7 @@ gunicorn==25.1.0 # via -r requirements/base.in identify==2.6.17 # via pre-commit -idna==3.11 +idna==3.13 # via # requests # trustme diff --git a/requirements/dev.txt b/requirements/dev.txt index bb588cdcae6..4d093bf6b9b 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -87,7 +87,7 @@ gunicorn==25.1.0 # via -r requirements/base.in identify==2.6.17 # via pre-commit -idna==3.11 +idna==3.13 # via # requests # trustme diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index 54885637357..1c34751a10b 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -18,7 +18,7 @@ click==8.3.1 # via towncrier docutils==0.21.2 # via sphinx -idna==3.11 +idna==3.13 # via requests imagesize==1.5.0 # via sphinx diff --git a/requirements/doc.txt b/requirements/doc.txt index 2fb865888b4..80626f3b482 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -18,7 +18,7 @@ click==8.3.1 # via towncrier docutils==0.21.2 # via sphinx -idna==3.11 +idna==3.13 # via requests imagesize==1.5.0 # via sphinx diff --git a/requirements/lint.txt b/requirements/lint.txt index 3026c475c36..aeb21bbe87c 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -39,7 +39,7 @@ freezegun==1.5.5 # via -r requirements/lint.in identify==2.6.17 # via pre-commit -idna==3.11 +idna==3.13 # via trustme iniconfig==2.3.0 # via pytest diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index f3a3df711ab..dc6ff7fbdfe 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -24,7 +24,7 @@ frozenlist==1.8.0 # via # -r requirements/runtime-deps.in # aiosignal -idna==3.11 +idna==3.13 # via yarl multidict==6.7.1 # via diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 2068e9166c6..3397c08c305 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -28,7 +28,7 @@ forbiddenfruit==0.1.4 # via blockbuster freezegun==1.5.5 # via -r requirements/test-common.in -idna==3.11 +idna==3.13 # via trustme iniconfig==2.3.0 # via pytest diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index c8495179b3e..407a152ca28 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -49,7 +49,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==25.1.0 # via -r requirements/base-ft.in -idna==3.11 +idna==3.13 # via # trustme # yarl diff --git a/requirements/test.txt b/requirements/test.txt index f119eaa25b5..14bb6dec7cf 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -49,7 +49,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==25.1.0 # via -r requirements/base.in -idna==3.11 +idna==3.13 # via # trustme # yarl From 985f27d032c1f271e351c4533dcb09689b1eaaf3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 15:30:01 +0000 Subject: [PATCH 034/191] Bump mypy from 1.19.1 to 1.20.2 (#12448) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mypy](https://github.com/python/mypy) from 1.19.1 to 1.20.2.
Changelog

Sourced from mypy's changelog.

Mypy 1.20.2

  • Use WAL with SQLite cache and fix close (Shantanu, PR 21154)
  • Adjust SQLite journal mode (Ivan Levkivskyi, PR 21217)
  • Correctly aggregate narrowing information on parent expressions (Shantanu, PR 21206)
  • Fix regression related to generic callables (Shantanu, PR 21208)
  • Fix regression by avoiding widening types in some contexts (Shantanu, PR 21242)
  • Fix slicing in non-strict optional mode (Shantanu, PR 21282)
  • mypyc: Fix match statement semantics for "or" pattern (Shantanu, PR 21156)
  • mypyc: Fix issue with module dunder attributes (Piotr Sawicki, PR 21275)
  • Initial support for Python 3.15.0a8 (Marc Mueller, PR 21255)

Acknowledgements

Thanks to all mypy contributors who contributed to this release:

  • A5rocks
  • Aaron Wieczorek
  • Adam Turner
  • Ali Hamdan
  • asce
  • BobTheBuidler
  • Brent Westbrook
  • Brian Schubert
  • bzoracler
  • Chris Burroughs
  • Christoph Tyralla
  • Colin Watson
  • Donghoon Nam
  • E. M. Bray
  • Emma Smith
  • Ethan Sarp
  • George Ogden
  • getzze
  • grayjk
  • Gregor Riepl
  • Ivan Levkivskyi
  • James Hilliard
  • James Le Cuirot
  • Jeremy Nimmer
  • Joren Hammudoglu
  • Kai (Kazuya Ito)
  • kaushal trivedi
  • Kevin Kannammalil
  • Lukas Geiger
  • Łukasz Langa
  • Marc Mueller
  • Michael R. Crusoe
  • michaelm-openai
  • Neil Schemenauer
  • Piotr Sawicki

... (truncated)

Commits

--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sam Bull --- aiohttp/client_reqrep.py | 2 +- aiohttp/web_log.py | 22 ++++++++++++---------- 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 +- 8 files changed, 19 insertions(+), 17 deletions(-) diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index aa0d48159c0..92b3529bf13 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -1054,7 +1054,7 @@ def update_headers(self, headers: LooseHeaders | None) -> None: if isinstance(headers, (dict, MultiDictProxy, MultiDict)): headers = headers.items() - for key, value in headers: # type: ignore[misc] + for key, value in headers: # type: ignore[str-unpack] # A special case for Host header if key in hdrs.HOST_ALL: self.headers[key] = value diff --git a/aiohttp/web_log.py b/aiohttp/web_log.py index 27b50adf6d6..9283cc08b12 100644 --- a/aiohttp/web_log.py +++ b/aiohttp/web_log.py @@ -4,15 +4,17 @@ import os import re import time as time_mod -from collections import namedtuple from collections.abc import Iterable -from typing import Callable, ClassVar +from typing import Callable, ClassVar, NamedTuple from .abc import AbstractAccessLogger from .web_request import BaseRequest from .web_response import StreamResponse -KeyMethod = namedtuple("KeyMethod", "key method") + +class KeyMethod(NamedTuple): + key: str | tuple[str, str] + method: Callable[[BaseRequest, StreamResponse, float], str] class AccessLogger(AbstractAccessLogger): @@ -198,7 +200,7 @@ def _format_D(request: BaseRequest, response: StreamResponse, time: float) -> st def _format_line( self, request: BaseRequest, response: StreamResponse, time: float - ) -> Iterable[tuple[str, Callable[[BaseRequest, StreamResponse, float], str]]]: + ) -> Iterable[tuple[str | tuple[str, str], str]]: return [(key, method(request, response, time)) for key, method in self._methods] @property @@ -212,17 +214,17 @@ def log(self, request: BaseRequest, response: StreamResponse, time: float) -> No fmt_info = self._format_line(request, response, time) values = list() - extra = dict() + extra: dict[str, str | dict[str, str]] = dict() for key, value in fmt_info: values.append(value) - if key.__class__ is str: + if isinstance(key, str): extra[key] = value else: - k1, k2 = key # type: ignore[misc] - dct = extra.get(k1, {}) # type: ignore[var-annotated,has-type] - dct[k2] = value # type: ignore[index,has-type] - extra[k1] = dct # type: ignore[has-type,assignment] + k1, k2 = key + dct: dict[str, str] = extra.get(k1, {}) # type: ignore[assignment] + dct[k2] = value + extra[k1] = dct self.logger.info(self._log_format % tuple(values), extra=extra) except Exception: diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 54b69c1677b..4a0c909b64a 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -119,7 +119,7 @@ multidict==6.7.1 # -r requirements/multidict.in # -r requirements/runtime-deps.in # yarl -mypy==1.19.1 ; implementation_name == "cpython" +mypy==1.20.2 ; implementation_name == "cpython" # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 4d093bf6b9b..fe600127a69 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -116,7 +116,7 @@ multidict==6.7.1 # via # -r requirements/runtime-deps.in # yarl -mypy==1.19.1 ; implementation_name == "cpython" +mypy==1.20.2 ; implementation_name == "cpython" # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/lint.txt b/requirements/lint.txt index aeb21bbe87c..d3d38e3791b 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -51,7 +51,7 @@ markdown-it-py==4.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -mypy==1.19.1 ; implementation_name == "cpython" +mypy==1.20.2 ; implementation_name == "cpython" # via -r requirements/lint.in mypy-extensions==1.1.0 # via mypy diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 3397c08c305..861d1d8b01e 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -40,7 +40,7 @@ markdown-it-py==4.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -mypy==1.19.1 ; implementation_name == "cpython" +mypy==1.20.2 ; implementation_name == "cpython" # via -r requirements/test-common.in mypy-extensions==1.1.0 # via mypy diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 407a152ca28..9e12a371431 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -67,7 +67,7 @@ multidict==6.7.1 # via # -r requirements/runtime-deps.in # yarl -mypy==1.19.1 ; implementation_name == "cpython" +mypy==1.20.2 ; implementation_name == "cpython" # via -r requirements/test-common.in mypy-extensions==1.1.0 # via mypy diff --git a/requirements/test.txt b/requirements/test.txt index 14bb6dec7cf..951b3eefdcb 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -67,7 +67,7 @@ multidict==6.7.1 # via # -r requirements/runtime-deps.in # yarl -mypy==1.19.1 ; implementation_name == "cpython" +mypy==1.20.2 ; implementation_name == "cpython" # via -r requirements/test-common.in mypy-extensions==1.1.0 # via mypy From f557ff00f46bbdab623f9c1cc8d30cf032e98f08 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 11:23:32 +0000 Subject: [PATCH 035/191] Bump charset-normalizer from 3.4.4 to 3.4.7 (#12454) Bumps [charset-normalizer](https://github.com/jawah/charset_normalizer) from 3.4.4 to 3.4.7.
Release notes

Sourced from charset-normalizer's releases.

Version 3.4.7

3.4.7 (2026-04-02)

Changed

  • Pre-built optimized version using mypy[c] v1.20.
  • Relax setuptools constraint to setuptools>=68,<82.1.

Fixed

  • Correctly remove SIG remnant in utf-7 decoded string. (#718) (#716)

Version 3.4.6

3.4.6 (2026-03-15)

Changed

  • Flattened the logic in charset_normalizer.md for higher performance. Removed eligible(..) and feed(...) in favor of feed_info(...).
  • Raised upper bound for mypy[c] to 1.20, for our optimized version.
  • Updated UNICODE_RANGES_COMBINED using Unicode blocks v17.

Fixed

  • Edge case where noise difference between two candidates can be almost insignificant. (#672)
  • CLI --normalize writing to wrong path when passing multiple files in. (#702)

Misc

  • Freethreaded pre-built wheels now shipped in PyPI starting with 3.14t. (#616)

Version 3.4.5

3.4.5 (2026-03-06)

Changed

  • Update setuptools constraint to setuptools>=68,<=82.
  • Raised upper bound of mypyc for the optional pre-built extension to v1.19.1

Fixed

  • Add explicit link to lib math in our optimized build. (#692)
  • Logger level not restored correctly for empty byte sequences. (#701)
  • TypeError when passing bytearray to from_bytes. (#703)

Misc

  • Applied safe micro-optimizations in both our noise detector and language detector.
  • Rewrote the query_yes_no function (inside CLI) to avoid using ambiguous licensed code.
  • Added cd.py submodule into mypyc optional compilation to reduce further the performance impact.

[!WARNING]
mypyc changed the usual binary output for the optimized wheel. Beware, especially if using PyInstaller or alike. See jawah/charset_normalizer#714

Changelog

Sourced from charset-normalizer's changelog.

3.4.7 (2026-04-02)

Changed

  • Pre-built optimized version using mypy[c] v1.20.
  • Relax setuptools constraint to setuptools>=68,<82.1.

Fixed

  • Correctly remove SIG remnant in utf-7 decoded string. (#718) (#716)

3.4.6 (2026-03-15)

Changed

  • Flattened the logic in charset_normalizer.md for higher performance. Removed eligible(..) and feed(...) in favor of feed_info(...).
  • Raised upper bound for mypy[c] to 1.20, for our optimized version.
  • Updated UNICODE_RANGES_COMBINED using Unicode blocks v17.

Fixed

  • Edge case where noise difference between two candidates can be almost insignificant. (#672)
  • CLI --normalize writing to wrong path when passing multiple files in. (#702)

Misc

  • Freethreaded pre-built wheels now shipped in PyPI starting with 3.14t. (#616)

3.4.5 (2026-03-06)

Changed

  • Update setuptools constraint to setuptools>=68,<=82.
  • Raised upper bound of mypyc for the optional pre-built extension to v1.19.1

Fixed

  • Add explicit link to lib math in our optimized build. (#692)
  • Logger level not restored correctly for empty byte sequences. (#701)
  • TypeError when passing bytearray to from_bytes. (#703)

Misc

  • Applied safe micro-optimizations in both our noise detector and language detector.
  • Rewrote the query_yes_no function (inside CLI) to avoid using ambiguous licensed code.
  • Added cd.py submodule into mypyc optional compilation to reduce further the performance impact.
Commits
  • 0f07891 Merge pull request #729 from jawah/release-3.4.7
  • fdbeb29 chore: update dev, and ci requirements
  • b66f922 chore: add ft classifier
  • f94249d chore: add test cases for utf_7 recent fix
  • 95c866f chore: bump version to 3.4.7
  • 4f429bb chore: bump mypy pre-commit to v1.20
  • b579cd6 fix: correctly remove SIG remnant in utf-7 decoded string
  • 58bf944 :arrow_up: Bump github/codeql-action from 4.32.4 to 4.35.1 (#728)
  • 44cf8a1 :arrow_up: Bump actions/download-artifact from 8.0.0 to 8.0.1 (#726)
  • 362bc20 :arrow_up: Bump docker/setup-qemu-action from 3.7.0 to 4.0.0 (#725)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=charset-normalizer&package-manager=pip&previous-version=3.4.4&new-version=3.4.7)](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/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 4a0c909b64a..acea040b78d 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -47,7 +47,7 @@ cffi==2.0.0 # pytest-codspeed cfgv==3.5.0 # via pre-commit -charset-normalizer==3.4.4 +charset-normalizer==3.4.7 # via requests click==8.3.1 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index fe600127a69..2faa726034c 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -47,7 +47,7 @@ cffi==2.0.0 # pytest-codspeed cfgv==3.5.0 # via pre-commit -charset-normalizer==3.4.4 +charset-normalizer==3.4.7 # via requests click==8.3.1 # via diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index 1c34751a10b..e05049eea51 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -12,7 +12,7 @@ babel==2.18.0 # via sphinx certifi==2026.2.25 # via requests -charset-normalizer==3.4.4 +charset-normalizer==3.4.7 # via requests click==8.3.1 # via towncrier diff --git a/requirements/doc.txt b/requirements/doc.txt index 80626f3b482..680ae3c836c 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -12,7 +12,7 @@ babel==2.18.0 # via sphinx certifi==2026.2.25 # via requests -charset-normalizer==3.4.4 +charset-normalizer==3.4.7 # via requests click==8.3.1 # via towncrier From ddcc11c7e69c8cccbeea0adfa41646ea0359baf5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 11:41:02 +0000 Subject: [PATCH 036/191] Bump gunicorn from 25.1.0 to 26.0.0 (#12456) Bumps [gunicorn](https://github.com/benoitc/gunicorn) from 25.1.0 to 26.0.0.
Release notes

Sourced from gunicorn's releases.

26.0.0

Breaking Changes

  • Eventlet worker removed: The eventlet worker class has been dropped. Migrate to gevent, gthread, or tornado.

New Features

  • ASGI Framework Compatibility Suite: New end-to-end compatibility test harness covering Starlette, FastAPI, Litestar, Quart, Sanic, and BlackSheep. Current grid passes 438/444 tests (98%).
  • ASGI Test Suite Expansion: 134 additional ASGI unit tests covering protocol semantics, lifespan, websockets, and chunked framing.

Security

  • HTTP/1.1 Request-Target Validation (RFC 9112 sections 3.2.3, 3.2.4):
    • Reject authority-form request-target outside CONNECT
    • Reject asterisk-form request-target outside OPTIONS
    • Reject relative-reference request-targets
  • Header Field Hardening (RFC 9110):
    • Reject control characters in header field-value (section 5.5)
    • Reject forbidden trailer field-names (section 6.5.1)
    • Reject Content-Length list form (RFC 9112 section 6.3)
  • Request Smuggling Hardening:
    • Tighten keepalive gate and scope finish_body byte cap
    • Keep _body_receiver alive across the keepalive smuggling gate so pipelined requests cannot re-enter a closed body
    • Address parser/protocol findings from a six-point WSGI/ASGI audit
  • PROXY Protocol (ASGI): Enforce proxy_allow_ips and tighten v1/v2 parsing in the ASGI callback parser.
  • Connection Draining: Drain the connection on close per RFC 9112 section 9.6 to prevent reset-on-close truncation.

Bug Fixes

  • Body Framing on HEAD/204/304:
    • Keep Content-Length on HEAD and 304 responses (#3621)
    • Drop body framing on HEAD/204/304 even when the framework set it
    • Warn once when an ASGI app emits a body for a no-body response
  • HTTP/2 ASGI:
    • Fix _handle_stream_ended to set _body_complete in the async HTTP/2 handler so request bodies finalize correctly on stream end
    • Add InvalidChunkExtension mapping and fast-parser support in ASGI tests (#3565)
  • HTTP/1.1 100-Continue: Stop adding Transfer-Encoding: chunked to 100-Continue interim responses.
  • WebSocket Close Handshake (RFC 6455):
    • Comply with the close handshake state machine
    • Close the transport after the close handshake completes
    • Fix binary send when the text key is None
  • Early Hints: Validate headers in the early_hints callback to match process_headers; pass only the header name to InvalidHeader (#3588).
  • ASGI Framework Fixes:
    • Fix ASGI disconnect handling for Django-style apps
    • Fix Litestar request handling (use raw ASGI receive for body/headers)
    • Fix Litestar HTTP endpoints for compatibility tests
    • Fix Quart headers endpoint to normalize keys to lowercase
    • Fix Quart WebSocket close test app (missing accept())
    • Fix duplicate Transfer-Encoding header for BlackSheep streaming

... (truncated)

Commits
  • 5d819cf release: 26.0.0
  • b45c70d Merge pull request #3611 from zc-mattcen/docs-typo
  • 99c8d48 Merge pull request #3623 from benoitc/chore/drop-eventlet-add-h2-uvloop-test-...
  • 5a655af Merge pull request #3622 from benoitc/test/docker-port-and-ipv4-fixes
  • 201df19 chore: remove eventlet worker; add h2 and uvloop to test deps
  • f4ac8e1 test: pass action name to dirty client and stabilize after TTOU spam
  • 54d38af test: unblock docker fixtures on macOS hosts
  • 68843c8 Merge pull request #3621 from benoitc/fix/asgi-preserve-content-length-on-hea...
  • 31f2618 Merge pull request #3620 from benoitc/fix/asgi-proxy-protocol-trust-and-parsing
  • 41ec752 fix: keep Content-Length on HEAD and 304 responses
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=gunicorn&package-manager=pip&previous-version=25.1.0&new-version=26.0.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 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/base-ft.txt | 2 +- requirements/base.txt | 2 +- requirements/constraints.txt | 2 +- requirements/dev.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt index 533c50b5f88..b90f244de52 100644 --- a/requirements/base-ft.txt +++ b/requirements/base-ft.txt @@ -24,7 +24,7 @@ frozenlist==1.8.0 # via # -r requirements/runtime-deps.in # aiosignal -gunicorn==25.1.0 +gunicorn==26.0.0 # via -r requirements/base-ft.in idna==3.13 # via yarl diff --git a/requirements/base.txt b/requirements/base.txt index abc83087f3d..a0cf0908ef5 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -24,7 +24,7 @@ frozenlist==1.8.0 # via # -r requirements/runtime-deps.in # aiosignal -gunicorn==25.1.0 +gunicorn==26.0.0 # via -r requirements/base.in idna==3.13 # via yarl diff --git a/requirements/constraints.txt b/requirements/constraints.txt index acea040b78d..cd2199ae759 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -85,7 +85,7 @@ frozenlist==1.8.0 # via # -r requirements/runtime-deps.in # aiosignal -gunicorn==25.1.0 +gunicorn==26.0.0 # via -r requirements/base.in identify==2.6.17 # via pre-commit diff --git a/requirements/dev.txt b/requirements/dev.txt index 2faa726034c..e84976610f9 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -83,7 +83,7 @@ frozenlist==1.8.0 # via # -r requirements/runtime-deps.in # aiosignal -gunicorn==25.1.0 +gunicorn==26.0.0 # via -r requirements/base.in identify==2.6.17 # via pre-commit diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 9e12a371431..efa93771272 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -47,7 +47,7 @@ frozenlist==1.8.0 # via # -r requirements/runtime-deps.in # aiosignal -gunicorn==25.1.0 +gunicorn==26.0.0 # via -r requirements/base-ft.in idna==3.13 # via diff --git a/requirements/test.txt b/requirements/test.txt index 951b3eefdcb..967c7a519e3 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -47,7 +47,7 @@ frozenlist==1.8.0 # via # -r requirements/runtime-deps.in # aiosignal -gunicorn==25.1.0 +gunicorn==26.0.0 # via -r requirements/base.in idna==3.13 # via From 9c5f2334a6e424fed5f11af4ac28ce8cc3e893e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 11:49:39 +0000 Subject: [PATCH 037/191] Bump pathspec from 1.0.4 to 1.1.1 (#12458) Bumps [pathspec](https://github.com/cpburnz/python-pathspec) from 1.0.4 to 1.1.1.
Release notes

Sourced from pathspec's releases.

v1.1.1

Release v1.1.1. See CHANGES.rst.

v1.1.0

Release v1.1.0. See CHANGES.rst.

Changelog

Sourced from pathspec's changelog.

1.1.1 (2026-04-26)

Improvements:

  • Improved type checking with mypy and pyright.

Bug fixes:

  • Fixed typing on PathSpec[TPattern] to PathSpec[TPattern_co].
  • Added missing variant type-hint type[Pattern] to PathSpec.from_lines() parameter pattern_factory.
  • Fixed possible type error when using + and += operators on PathSpec.

1.1.0 (2026-04-22)

New features:

  • Issue [#108](https://github.com/cpburnz/python-pathspec/issues/108)_: Specialize pattern type for PathSpec as PathSpec[TPattern] for better debugging of PathSpec().patterns.

Bug fixes:

  • Issue [#93](https://github.com/cpburnz/python-pathspec/issues/93)_: Git discards invalid range notation. GitIgnoreSpecPattern now discards patterns with invalid range notation like Git.
  • Pull [#106](https://github.com/cpburnz/python-pathspec/issues/106)_: Fix escape() not escaping backslash characters.

Improvements:

  • Pull [#110](https://github.com/cpburnz/python-pathspec/issues/110)_: Nicer debug print outs (and str for regex pattern).

.. _Pull [#106](https://github.com/cpburnz/python-pathspec/issues/106): cpburnz/python-pathspec#106 .. _Issue [#108](https://github.com/cpburnz/python-pathspec/issues/108): cpburnz/python-pathspec#108 .. _Pull [#110](https://github.com/cpburnz/python-pathspec/issues/110): cpburnz/python-pathspec#110

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pathspec&package-manager=pip&previous-version=1.0.4&new-version=1.1.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 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-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 cd2199ae759..0f4b1e6ad86 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -134,7 +134,7 @@ packaging==26.2 # pytest # sphinx # wheel -pathspec==1.0.4 +pathspec==1.1.1 # via mypy pip-tools==7.5.3 # via -r requirements/dev.in diff --git a/requirements/dev.txt b/requirements/dev.txt index e84976610f9..360fd8f7668 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -131,7 +131,7 @@ packaging==26.2 # pytest # sphinx # wheel -pathspec==1.0.4 +pathspec==1.1.1 # via mypy pip-tools==7.5.3 # via -r requirements/dev.in diff --git a/requirements/lint.txt b/requirements/lint.txt index d3d38e3791b..00eb08ea7d8 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -59,7 +59,7 @@ nodeenv==1.10.0 # via pre-commit packaging==26.2 # via pytest -pathspec==1.0.4 +pathspec==1.1.1 # via mypy platformdirs==4.9.2 # via diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 861d1d8b01e..284d1f94af7 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -46,7 +46,7 @@ mypy-extensions==1.1.0 # via mypy packaging==26.2 # via pytest -pathspec==1.0.4 +pathspec==1.1.1 # via mypy pkgconfig==1.5.5 # via -r requirements/test-common.in diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index efa93771272..5ce1ed51055 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -75,7 +75,7 @@ packaging==26.2 # via # gunicorn # pytest -pathspec==1.0.4 +pathspec==1.1.1 # via mypy pkgconfig==1.5.5 # via -r requirements/test-common.in diff --git a/requirements/test.txt b/requirements/test.txt index 967c7a519e3..5af2b34871b 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -75,7 +75,7 @@ packaging==26.2 # via # gunicorn # pytest -pathspec==1.0.4 +pathspec==1.1.1 # via mypy pkgconfig==1.5.5 # via -r requirements/test-common.in From 3a351ac6c384a1d55c18ed5d5faf75e3e96307e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 11:54:34 +0000 Subject: [PATCH 038/191] Bump pydantic from 2.12.5 to 2.13.3 (#12461) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.12.5 to 2.13.3.
Release notes

Sourced from pydantic's releases.

v2.13.3 2026-04-20

v2.13.3 (2026-04-20)

What's Changed

Fixes

Full Changelog: https://github.com/pydantic/pydantic/compare/v2.13.2...v2.13.3

v2.13.2 2026-04-17

v2.13.2 (2026-04-17)

What's Changed

Fixes

  • Fix ValidationInfo.field_name missing with model_validate_json() by @​Viicos in #13084

Full Changelog: https://github.com/pydantic/pydantic/compare/v2.13.1...v2.13.2

v2.13.1 2026-04-15

v2.13.1 (2026-04-15)

What's Changed

Fixes

Full Changelog: https://github.com/pydantic/pydantic/compare/v2.13.0...v2.13.1

v2.13.0 2026-04-13

v2.13.0 (2026-04-13)

The highlights of the v2.13 release are available in the blog post. Several minor changes (considered non-breaking changes according to our versioning policy) are also included in this release. Make sure to look into them before upgrading.

This release contains the updated pydantic.v1 namespace, matching version 1.10.26 which includes support for Python 3.14.

What's Changed

See the beta releases for all changes sinces 2.12.

Packaging

  • Add zizmor for GitHub Actions workflow linting by @​Viicos in #13039
  • Update jiter to v0.14.0 to fix a segmentation fault on musl Linux by @​Viicos in #13064

... (truncated)

Changelog

Sourced from pydantic's changelog.

v2.13.3 (2026-04-20)

GitHub release

What's Changed

Fixes

v2.13.2 (2026-04-17)

GitHub release

What's Changed

Fixes

  • Fix ValidationInfo.field_name missing with model_validate_json() by @​Viicos in #13084

v2.13.1 (2026-04-15)

GitHub release

What's Changed

Fixes

v2.13.0 (2026-04-13)

GitHub release

The highlights of the v2.13 release are available in the blog post. Several minor changes (considered non-breaking changes according to our versioning policy) are also included in this release. Make sure to look into them before upgrading.

This release contains the updated pydantic.v1 namespace, matching version 1.10.26 which includes support for Python 3.14.

What's Changed

See the beta releases for all changes sinces 2.12.

New Features

  • Allow default factories of private attributes to take validated model data by @​Viicos in #13013

Changes

... (truncated)

Commits
  • 9e9a111 Fix backported test
  • 1ec8c6a Prepare release v2.13.3
  • fb4f204 Handle AttributeError subclasses with from_attributes
  • ca3ddd1 Prepare release v2.13.2
  • 000e823 Fix ValidationInfo.field_name missing with model_validate_json()
  • d45d8be Prepare release 2.13.1
  • 54aca60 Fix ValidationInfo.data missing with model_validate_json()
  • 46bf4fa Fix Pydantic release workflow (#13067)
  • 1b359ed Prepare release v2.13.0 (#13065)
  • b1bf194 Fix model equality when using runtime extra configuration (#13062)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pydantic&package-manager=pip&previous-version=2.12.5&new-version=2.13.3)](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 0f4b1e6ad86..86955541a23 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -160,9 +160,9 @@ pycares==4.11.0 # via aiodns pycparser==3.0 # via cffi -pydantic==2.12.5 +pydantic==2.13.3 # via python-on-whales -pydantic-core==2.41.5 +pydantic-core==2.46.3 # via pydantic pyenchant==3.3.0 # via sphinxcontrib-spelling diff --git a/requirements/dev.txt b/requirements/dev.txt index 360fd8f7668..c8a99f52b5c 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -157,9 +157,9 @@ pycares==4.11.0 # via aiodns pycparser==3.0 # via cffi -pydantic==2.12.5 +pydantic==2.13.3 # via python-on-whales -pydantic-core==2.41.5 +pydantic-core==2.46.3 # via pydantic pygments==2.19.2 # via diff --git a/requirements/lint.txt b/requirements/lint.txt index 00eb08ea7d8..53d0a12ee2f 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -73,9 +73,9 @@ pycares==4.11.0 # via aiodns pycparser==3.0 # via cffi -pydantic==2.12.5 +pydantic==2.13.3 # via python-on-whales -pydantic-core==2.41.5 +pydantic-core==2.46.3 # via pydantic pygments==2.19.2 # via diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 284d1f94af7..4f7b6917386 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -58,9 +58,9 @@ proxy-py==2.4.10 # via -r requirements/test-common.in pycparser==3.0 # via cffi -pydantic==2.12.5 +pydantic==2.13.3 # via python-on-whales -pydantic-core==2.41.5 +pydantic-core==2.46.3 # via pydantic pygments==2.19.2 # via diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 5ce1ed51055..dc924e08f12 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -93,9 +93,9 @@ pycares==4.11.0 # via aiodns pycparser==3.0 # via cffi -pydantic==2.12.5 +pydantic==2.13.3 # via python-on-whales -pydantic-core==2.41.5 +pydantic-core==2.46.3 # via pydantic pygments==2.19.2 # via diff --git a/requirements/test.txt b/requirements/test.txt index 5af2b34871b..13304c307e7 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -93,9 +93,9 @@ pycares==4.11.0 # via aiodns pycparser==3.0 # via cffi -pydantic==2.12.5 +pydantic==2.13.3 # via python-on-whales -pydantic-core==2.41.5 +pydantic-core==2.46.3 # via pydantic pygments==2.19.2 # via From 867103f8507911c20ad22a95ffc3973966ecee40 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 12:08:16 +0000 Subject: [PATCH 039/191] Bump requests from 2.32.5 to 2.33.1 (#12463) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [requests](https://github.com/psf/requests) from 2.32.5 to 2.33.1.
Release notes

Sourced from requests's releases.

v2.33.1

2.33.1 (2026-03-30)

Bugfixes

  • Fixed test cleanup for CVE-2026-25645 to avoid leaving unnecessary files in the tmp directory. (#7305)
  • Fixed Content-Type header parsing for malformed values. (#7309)
  • Improved error consistency for malformed header values. (#7308)

New Contributors

Full Changelog: https://github.com/psf/requests/blob/main/HISTORY.md#2331-2026-03-30

v2.33.0

2.33.0 (2026-03-25)

Announcements

  • 📣 Requests is adding inline types. If you have a typed code base that uses Requests, please take a look at #7271. Give it a try, and report any gaps or feedback you may have in the issue. 📣

Security

  • CVE-2026-25645 requests.utils.extract_zipped_paths now extracts contents to a non-deterministic location to prevent malicious file replacement. This does not affect default usage of Requests, only applications calling the utility function directly.

Improvements

  • Migrated to a PEP 517 build system using setuptools. (#7012)

Bugfixes

  • Fixed an issue where an empty netrc entry could cause malformed authentication to be applied to Requests on Python 3.11+. (#7205)

Deprecations

  • Dropped support for Python 3.9 following its end of support. (#7196)

Documentation

  • Various typo fixes and doc improvements.

New Contributors

Full Changelog: https://github.com/psf/requests/blob/main/HISTORY.md#2330-2026-03-25

Changelog

Sourced from requests's changelog.

2.33.1 (2026-03-30)

Bugfixes

  • Fixed test cleanup for CVE-2026-25645 to avoid leaving unnecessary files in the tmp directory. (#7305)
  • Fixed Content-Type header parsing for malformed values. (#7309)
  • Improved error consistency for malformed header values. (#7308)

2.33.0 (2026-03-25)

Announcements

  • 📣 Requests is adding inline types. If you have a typed code base that uses Requests, please take a look at #7271. Give it a try, and report any gaps or feedback you may have in the issue. 📣

Security

  • CVE-2026-25645 requests.utils.extract_zipped_paths now extracts contents to a non-deterministic location to prevent malicious file replacement. This does not affect default usage of Requests, only applications calling the utility function directly.

Improvements

  • Migrated to a PEP 517 build system using setuptools. (#7012)

Bugfixes

  • Fixed an issue where an empty netrc entry could cause malformed authentication to be applied to Requests on Python 3.11+. (#7205)

Deprecations

  • Dropped support for Python 3.9 following its end of support. (#7196)

Documentation

  • Various typo fixes and doc improvements.
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/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 86955541a23..261a4d53bad 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -209,7 +209,7 @@ re-assert==1.1.0 # via -r requirements/test-common.in regex==2026.2.28 # via re-assert -requests==2.32.5 +requests==2.33.1 # via # sphinx # sphinxcontrib-spelling diff --git a/requirements/dev.txt b/requirements/dev.txt index c8a99f52b5c..0f24c9c2efd 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -204,7 +204,7 @@ re-assert==1.1.0 # via -r requirements/test-common.in regex==2026.2.28 # via re-assert -requests==2.32.5 +requests==2.33.1 # via sphinx rich==14.3.3 # via pytest-codspeed diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index e05049eea51..080bbd7f499 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -34,7 +34,7 @@ pyenchant==3.3.0 # via sphinxcontrib-spelling pygments==2.19.2 # via sphinx -requests==2.32.5 +requests==2.33.1 # via # sphinx # sphinxcontrib-spelling diff --git a/requirements/doc.txt b/requirements/doc.txt index 680ae3c836c..b52661badb7 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -32,7 +32,7 @@ packaging==26.2 # via sphinx pygments==2.19.2 # via sphinx -requests==2.32.5 +requests==2.33.1 # via sphinx snowballstemmer==3.0.1 # via sphinx From a7a8a485432a6f1f0fd2dd7ee8b41c996852b721 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 12:12:47 +0000 Subject: [PATCH 040/191] Bump pytest-codspeed from 4.3.0 to 4.5.0 (#12462) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [//]: # (dependabot-start) ⚠️ **Dependabot is rebasing this PR** ⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Bumps [pytest-codspeed](https://github.com/CodSpeedHQ/pytest-codspeed) from 4.3.0 to 4.5.0.
Release notes

Sourced from pytest-codspeed's releases.

v4.5.0

What's Changed

This release adds first support for macOS walltime.

Please note that profiling and other instruments are not yet available on macOS and will come in a later update.

Full Changelog: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v4.4.0...v4.5.0

v4.4.0

What's Changed

We now collect buildtime and runtime environment data to warn users about differences in their runtime environment when comparing two runs against one another.

This data includes toolchain metadata like version and build options, as well as a list of dynamically loaded linked libraries.

New Contributors

Full Changelog: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v4.3.0...v4.4.0

Changelog

Sourced from pytest-codspeed's changelog.

[4.5.0] - 2026-04-28

⚙️ Internals

[4.4.0] - 2026-04-14

🚀 Features

🐛 Bug Fixes

⚙️ Internals

Commits
  • 2dc7398 Release v4.5.0 🚀
  • 48f8ce8 chore: pre-build macos binary
  • facf050 chore: bump instrument-hooks submodule to use int32_t as pid
  • d42de44 ci: add macos integration test
  • 34048c7 Release v4.4.0 🚀
  • bf7bd37 feat: collect Python toolchain information via instrument hooks environment API
  • 48a4822 fix: Exclude setup time from benchmark in walltime mode (#114)
  • aa267f3 ci: bump the python pinned python version
  • e92999a fix: fix segfault caused by multiple activate_stack_trampoline
  • c02b664 chore: add CONTRIBUTING.md
  • See full diff in compare view

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 261a4d53bad..73bfad054ee 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -183,7 +183,7 @@ pytest==9.0.2 # pytest-cov # pytest-mock # pytest-xdist -pytest-codspeed==4.3.0 +pytest-codspeed==4.5.0 # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 0f24c9c2efd..564312f1e5c 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -178,7 +178,7 @@ pytest==9.0.2 # pytest-cov # pytest-mock # pytest-xdist -pytest-codspeed==4.3.0 +pytest-codspeed==4.5.0 # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/lint.txt b/requirements/lint.txt index 53d0a12ee2f..09f3ac3d6fd 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -86,7 +86,7 @@ pytest==9.0.2 # -r requirements/lint.in # pytest-codspeed # pytest-mock -pytest-codspeed==4.3.0 +pytest-codspeed==4.5.0 # via -r requirements/lint.in pytest-mock==3.15.1 # via -r requirements/lint.in diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 4f7b6917386..b189234a462 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -73,7 +73,7 @@ pytest==9.0.2 # pytest-cov # pytest-mock # pytest-xdist -pytest-codspeed==4.3.0 +pytest-codspeed==4.5.0 # via -r requirements/test-common.in pytest-cov==7.0.0 # via -r requirements/test-common.in diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index dc924e08f12..ba2892bc3e4 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -108,7 +108,7 @@ pytest==9.0.2 # pytest-cov # pytest-mock # pytest-xdist -pytest-codspeed==4.3.0 +pytest-codspeed==4.5.0 # via -r requirements/test-common.in pytest-cov==7.0.0 # via -r requirements/test-common.in diff --git a/requirements/test.txt b/requirements/test.txt index 13304c307e7..40c27fcb009 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -108,7 +108,7 @@ pytest==9.0.2 # pytest-cov # pytest-mock # pytest-xdist -pytest-codspeed==4.3.0 +pytest-codspeed==4.5.0 # via -r requirements/test-common.in pytest-cov==7.0.0 # via -r requirements/test-common.in From 5840d7b1930a93355f48da31b27882de7f011188 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 12:17:42 +0000 Subject: [PATCH 041/191] Bump pre-commit from 4.5.1 to 4.6.0 (#12464) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 4.5.1 to 4.6.0.
Release notes

Sourced from pre-commit's releases.

pre-commit v4.6.0

Features

  • pre-commit hook-impl: allow --hook-dir to be missing to enable easier usage with git 2.54+ git hooks.

Fixes

Changelog

Sourced from pre-commit's changelog.

4.6.0 - 2026-04-21

Features

  • pre-commit hook-impl: allow --hook-dir to be missing to enable easier usage with git 2.54+ git hooks.

Fixes

Commits
  • f35134b v4.6.0
  • 2a51ffc Merge pull request #3662 from pre-commit/hook-impl-optional-hook-dir
  • d7dee32 make --hook-dir optional for hook-impl
  • 965aeb1 Merge pull request #3661 from pre-commit/hook-impl-required
  • 2eacc06 --hook-type is required for hook-impl
  • f5678bf Merge pull request #3657 from pre-commit/pre-commit-ci-update-config
  • 054cc5b [pre-commit.ci] pre-commit autoupdate
  • 5c0f302 Merge pull request #3652 from pre-commit/pre-commit-ci-update-config
  • a5d9114 [pre-commit.ci] pre-commit autoupdate
  • 129a1f5 Merge pull request #3641 from pre-commit/mxr-patch-1
  • Additional commits viewable in compare view

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 73bfad054ee..0d80c99aa17 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -148,7 +148,7 @@ pluggy==1.6.0 # via # pytest # pytest-cov -pre-commit==4.5.1 +pre-commit==4.6.0 # via -r requirements/lint.in propcache==0.4.1 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index 564312f1e5c..cb93344d441 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -145,7 +145,7 @@ pluggy==1.6.0 # via # pytest # pytest-cov -pre-commit==4.5.1 +pre-commit==4.6.0 # via -r requirements/lint.in propcache==0.4.1 # via diff --git a/requirements/lint.txt b/requirements/lint.txt index 09f3ac3d6fd..a59e7dcbb60 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -67,7 +67,7 @@ platformdirs==4.9.2 # virtualenv pluggy==1.6.0 # via pytest -pre-commit==4.5.1 +pre-commit==4.6.0 # via -r requirements/lint.in pycares==4.11.0 # via aiodns From 929a286eef3869505e15ccde0bb8750e950589fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 12:37:18 +0000 Subject: [PATCH 042/191] Bump pytest-cov from 7.0.0 to 7.1.0 (#12453) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 7.0.0 to 7.1.0.
Changelog

Sourced from pytest-cov's changelog.

7.1.0 (2026-03-21)

  • Fixed total coverage computation to always be consistent, regardless of reporting settings. Previously some reports could produce different total counts, and consequently can make --cov-fail-under behave different depending on reporting options. See [#641](https://github.com/pytest-dev/pytest-cov/issues/641) <https://github.com/pytest-dev/pytest-cov/issues/641>_.

  • Improve handling of ResourceWarning from sqlite3.

    The plugin adds warning filter for sqlite3 ResourceWarning unclosed database (since 6.2.0). It checks if there is already existing plugin for this message by comparing filter regular expression. When filter is specified on command line the message is escaped and does not match an expected message. A check for an escaped regular expression is added to handle this case.

    With this fix one can suppress ResourceWarning from sqlite3 from command line::

    pytest -W "ignore:unclosed database in <sqlite3.Connection object at:ResourceWarning" ...

  • Various improvements to documentation. Contributed by Art Pelling in [#718](https://github.com/pytest-dev/pytest-cov/issues/718) <https://github.com/pytest-dev/pytest-cov/pull/718>_ and "vivodi" in [#738](https://github.com/pytest-dev/pytest-cov/issues/738) <https://github.com/pytest-dev/pytest-cov/pull/738>. Also closed [#736](https://github.com/pytest-dev/pytest-cov/issues/736) <https://github.com/pytest-dev/pytest-cov/issues/736>.

  • Fixed some assertions in tests. Contributed by in Markéta Machová in [#722](https://github.com/pytest-dev/pytest-cov/issues/722) <https://github.com/pytest-dev/pytest-cov/pull/722>_.

  • Removed unnecessary coverage configuration copying (meant as a backup because reporting commands had configuration side-effects before coverage 5.0).

Commits
  • 66c8a52 Bump version: 7.0.0 → 7.1.0
  • f707662 Make the examples use pypy 3.11.
  • 6049a78 Make context test use the old ctracer (seems the new sysmon tracer behaves di...
  • 8ebf20b Update changelog.
  • 861d30e Remove the backup context manager - shouldn't be needed since coverage 5.0, ...
  • fd4c956 Pass the precision on the nulled total (seems that there's some caching goion...
  • 78c9c4e Only run the 3.9 on older deps.
  • 4849a92 Punctuation.
  • 197c35e Update changelog and hopefully I don't forget to publish release again :))
  • 14dc1c9 Update examples to use 3.11 and make the adhoc layout example look a bit more...
  • Additional commits viewable in compare view

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.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 0d80c99aa17..d696c55c624 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -187,7 +187,7 @@ pytest-codspeed==4.5.0 # via # -r requirements/lint.in # -r requirements/test-common.in -pytest-cov==7.0.0 +pytest-cov==7.1.0 # via -r requirements/test-common.in pytest-mock==3.15.1 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index cb93344d441..43550edc62a 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -182,7 +182,7 @@ pytest-codspeed==4.5.0 # via # -r requirements/lint.in # -r requirements/test-common.in -pytest-cov==7.0.0 +pytest-cov==7.1.0 # via -r requirements/test-common.in pytest-mock==3.15.1 # via diff --git a/requirements/test-common.txt b/requirements/test-common.txt index b189234a462..28caba8dc46 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -75,7 +75,7 @@ pytest==9.0.2 # pytest-xdist pytest-codspeed==4.5.0 # via -r requirements/test-common.in -pytest-cov==7.0.0 +pytest-cov==7.1.0 # via -r requirements/test-common.in pytest-mock==3.15.1 # via -r requirements/test-common.in diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index ba2892bc3e4..4a4aaa6e44f 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -110,7 +110,7 @@ pytest==9.0.2 # pytest-xdist pytest-codspeed==4.5.0 # via -r requirements/test-common.in -pytest-cov==7.0.0 +pytest-cov==7.1.0 # via -r requirements/test-common.in pytest-mock==3.15.1 # via -r requirements/test-common.in diff --git a/requirements/test.txt b/requirements/test.txt index 40c27fcb009..999599c98d8 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -110,7 +110,7 @@ pytest==9.0.2 # pytest-xdist pytest-codspeed==4.5.0 # via -r requirements/test-common.in -pytest-cov==7.0.0 +pytest-cov==7.1.0 # via -r requirements/test-common.in pytest-mock==3.15.1 # via -r requirements/test-common.in From 437adfd8aba93ddda3d7b8e6c2b187526227c8cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 10:59:53 +0000 Subject: [PATCH 043/191] Bump attrs from 25.4.0 to 26.1.0 (#12467) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [attrs](https://github.com/python-attrs/attrs) from 25.4.0 to 26.1.0.
Release notes

Sourced from attrs's releases.

26.1.0

Highlights

The main outward change here only affects people using field transformers, but it should be a nice quality of life improvement!

Full changelog below!

Special Thanks

This release would not be possible without my generous sponsors! Thank you to all of you making sustainable maintenance possible! If you would like to join them, go to https://github.com/sponsors/hynek and check out the sweet perks!

Above and Beyond

Variomedia AG (@variomedia), Tidelift (@tidelift), Kraken Tech (@kraken-tech), Privacy Solutions GmbH (@privacy-solutions), FilePreviews (@filepreviews), Ecosystems (@ecosyste-ms), TestMu AI Open Source Office (Formerly LambdaTest) (@LambdaTest-Inc), Doist (@Doist), Daniel Fortunov (@asqui), and Kevin P. Fleming (@kpfleming).

Maintenance Sustainers

Buttondown (@buttondown), Christopher Dignam (@chdsbd), Magnus Watn (@magnuswatn), David Cramer (@dcramer), Rivo Laks (@rivol), Polar (@polarsource), Mike Fiedler (@miketheman), Duncan Hill (@cricalix), Colin Marquardt (@cmarqu), Pieter Swinkels (@swinkels), Nick Libertini (@libertininick), Brian M. Dennis (@crossjam), Celebrity News AG (@celebritynewsag), The Westervelt Company (@westerveltco), Sławomir Ehlert (@slafs), Mostafa Khalil (@khadrawy), Filip Mularczyk (@mukiblejlok), Thomas Klinger (@thmsklngr), Andreas Poehlmann (@ap--), August Trapper Bigelow (@atbigelow), Carlton Gibson (@carltongibson), and Roboflow (@roboflow).

Full Changelog

Backwards-incompatible Changes

  • Field aliases are now resolved before calling field_transformer, so transformers receive fully populated Attribute objects with usable alias values instead of None. The new Attribute.alias_is_default flag indicates whether the alias was auto-generated (True) or explicitly set by the user (False). #1509

Changes

  • Fix type annotations for attrs.validators.optional(), so it no longer rejects tuples with more than one validator. #1496
  • The attrs.validators.disabled() contextmanager can now be nested. #1513
  • Frozen classes can set on_setattr=attrs.setters.NO_OP in addition to None. #1515
  • It's now possible to pass attrs instances in addition to attrs classes to attrs.fields(). #1529

This release contains contributions from @​bysiber, @​DavidCEllis, @​finite-state-machine, @​hynek, @​veeceey, and @​vstinner.

Artifact Attestations

You can verify this release's artifact attestions using GitHub's CLI tool by downloading the sdist and wheel from PyPI and running:

$ gh attestation verify --owner python-attrs
attrs-26.1.0.tar.gz

... (truncated)

Changelog

Sourced from attrs's changelog.

26.1.0 - 2026-03-19

Backwards-incompatible Changes

  • Field aliases are now resolved before calling field_transformer, so transformers receive fully populated Attribute objects with usable alias values instead of None. The new Attribute.alias_is_default flag indicates whether the alias was auto-generated (True) or explicitly set by the user (False). #1509

Changes

  • Fix type annotations for attrs.validators.optional(), so it no longer rejects tuples with more than one validator. #1496
  • The attrs.validators.disabled() contextmanager can now be nested. #1513
  • Frozen classes can set on_setattr=attrs.setters.NO_OP in addition to None. #1515
  • It's now possible to pass attrs instances in addition to attrs classes to attrs.fields(). #1529
Commits
  • 7bfc49e Prepare 26.1.0
  • 31e0286 Update test_validators.py for Python 3.15a7 (#1530)
  • 48b8611 Add instance support to attrs.fields() (#1529)
  • 3a68d49 dev: document missing git tags failure mode
  • a572c3a Allow field(on_setattr=NO_OP) on frozen classes
  • af9c510 Fix validators.disabled() to save/restore state on nesting (#1513)
  • ab7f8b2 update dev
  • ce89f5d Fix message passing in frozen errors
  • eccd966 Fix optional validator to accept tuples of len > 1 (#1496)
  • e92fe52 policies: tighten screws (#1528)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=attrs&package-manager=pip&previous-version=25.4.0&new-version=26.1.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 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/base-ft.txt | 2 +- requirements/base.txt | 2 +- requirements/constraints.txt | 2 +- requirements/dev.txt | 2 +- requirements/runtime-deps.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt index b90f244de52..1f6f712fee4 100644 --- a/requirements/base-ft.txt +++ b/requirements/base-ft.txt @@ -12,7 +12,7 @@ aiosignal==1.4.0 # via -r requirements/runtime-deps.in async-timeout==5.0.1 ; python_version < "3.11" # via -r requirements/runtime-deps.in -attrs==25.4.0 +attrs==26.1.0 # via -r requirements/runtime-deps.in backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" # via -r requirements/runtime-deps.in diff --git a/requirements/base.txt b/requirements/base.txt index a0cf0908ef5..a8a4c998885 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -12,7 +12,7 @@ aiosignal==1.4.0 # via -r requirements/runtime-deps.in async-timeout==5.0.1 ; python_version < "3.11" # via -r requirements/runtime-deps.in -attrs==25.4.0 +attrs==26.1.0 # via -r requirements/runtime-deps.in backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" # via -r requirements/runtime-deps.in diff --git a/requirements/constraints.txt b/requirements/constraints.txt index d696c55c624..e5ed250ccd9 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -22,7 +22,7 @@ async-timeout==5.0.1 ; python_version < "3.11" # via # -r requirements/runtime-deps.in # valkey -attrs==25.4.0 +attrs==26.1.0 # via -r requirements/runtime-deps.in babel==2.18.0 # via sphinx diff --git a/requirements/dev.txt b/requirements/dev.txt index 43550edc62a..7942783e26f 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -22,7 +22,7 @@ async-timeout==5.0.1 ; python_version < "3.11" # via # -r requirements/runtime-deps.in # valkey -attrs==25.4.0 +attrs==26.1.0 # via -r requirements/runtime-deps.in babel==2.18.0 # via sphinx diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index dc6ff7fbdfe..9eb626271a1 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -12,7 +12,7 @@ aiosignal==1.4.0 # via -r requirements/runtime-deps.in async-timeout==5.0.1 ; python_version < "3.11" # via -r requirements/runtime-deps.in -attrs==25.4.0 +attrs==26.1.0 # via -r requirements/runtime-deps.in backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" # via -r requirements/runtime-deps.in diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 4a4aaa6e44f..77bef1382ed 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -14,7 +14,7 @@ annotated-types==0.7.0 # via pydantic async-timeout==5.0.1 ; python_version < "3.11" # via -r requirements/runtime-deps.in -attrs==25.4.0 +attrs==26.1.0 # via -r requirements/runtime-deps.in backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" # via -r requirements/runtime-deps.in diff --git a/requirements/test.txt b/requirements/test.txt index 999599c98d8..a2533087c0e 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -14,7 +14,7 @@ annotated-types==0.7.0 # via pydantic async-timeout==5.0.1 ; python_version < "3.11" # via -r requirements/runtime-deps.in -attrs==25.4.0 +attrs==26.1.0 # via -r requirements/runtime-deps.in backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" # via -r requirements/runtime-deps.in From b740ddec27a92667e004811b972eb8896704f53b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 11:41:19 +0000 Subject: [PATCH 044/191] Bump virtualenv from 21.1.0 to 21.3.1 (#12469) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [virtualenv](https://github.com/pypa/virtualenv) from 21.1.0 to 21.3.1.
Release notes

Sourced from virtualenv's releases.

21.3.1

What's Changed

Full Changelog: https://github.com/pypa/virtualenv/compare/21.3.0...21.3.1

21.3.0

What's Changed

New Contributors

Full Changelog: https://github.com/pypa/virtualenv/compare/21.2.4...21.3.0

21.2.4

What's Changed

Full Changelog: https://github.com/pypa/virtualenv/compare/21.2.3...21.2.4

21.2.3

Full Changelog: https://github.com/pypa/virtualenv/compare/21.2.2...21.2.3

21.2.2

What's Changed

... (truncated)

Changelog

Sourced from virtualenv's changelog.

Bugfixes - 21.3.1

  • Upgrade embedded wheels:

    • pip to 26.1.1 from 26.1 (:issue:3138)

v21.3.0 (2026-04-27)


Features - 21.3.0

  • Re-introduce xonsh shell activator (activate.xsh) previously removed in 20.7.0, and make the plugin loader prefer virtualenv's built-in entry points so a third-party package cannot override them by registering a duplicate name. (:issue:3003)

Bugfixes - 21.3.0

  • Upgrade embedded wheels:

    • pip to 26.1 (:issue:3132)

v21.2.4 (2026-04-14)


Bugfixes - 21.2.4

  • Security hardening: validate each entry of a seed wheel archive before extracting it so a tampered wheel cannot escape the app-data image directory via an absolute path or .. traversal. (:issue:3118)
  • Security hardening: verify the SHA-256 of every bundled seed wheel when it is loaded so a corrupted or tampered file on disk fails loud instead of being handed to pip. The hash table is generated alongside BUNDLE_SUPPORT by tasks/upgrade_wheels.py. (:issue:3119)
  • Security hardening: validate the distribution name and version specifier passed to pip download when acquiring a seed wheel so extras, pip flags, or shell metacharacters cannot be smuggled into the subprocess command line. (:issue:3120)
  • Security hardening: replace the string-prefix containment check in virtualenv.util.zipapp with Path.relative_to so the zipapp extraction helpers refuse any path that does not resolve under the archive root. (:issue:3121)
  • Security hardening: do not silently fall back to an unverified HTTPS context when the periodic update request to PyPI fails TLS verification. The returned metadata drives which wheel version virtualenv considers "up to date", so accepting an unverified response lets a network-level attacker suppress security updates. Set VIRTUALENV_PERIODIC_UPDATE_INSECURE=1 to restore the previous behavior on hosts with broken trust stores. (:issue:3122)

... (truncated)

Commits
  • 12ab495 release 21.3.1
  • 22eadc4 [pre-commit.ci] pre-commit autoupdate (#3137)
  • 6651daf 🐛 fix(seed): bump embedded pip to 26.1.1 (#3138)
  • 936a36a 👷 ci: retry transient apt failures on Linux (#3139)
  • cb5a7d1 [pre-commit.ci] pre-commit autoupdate (#3133)
  • e917cc2 release 21.3.0
  • 21152f1 Upgrade embedded pip/setuptools/wheel (#3132)
  • 096bdcd chore(deps): bump astral-sh/setup-uv from 8.0.0 to 8.1.0 (#3131)
  • 01610dc docs: Add usage instruction for Xonsh activation (#3130)
  • fb6ec7c 🐛 fix(test): prevent PowerShell activation test from crashing xdist workers o...
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=virtualenv&package-manager=pip&previous-version=21.1.0&new-version=21.3.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 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 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index e5ed250ccd9..f5575bf5e25 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -197,7 +197,7 @@ pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 # via freezegun -python-discovery==1.1.0 +python-discovery==1.3.0 # via virtualenv python-on-whales==0.80.0 # via @@ -285,7 +285,7 @@ uvloop==0.21.0 ; platform_system != "Windows" # -r requirements/lint.in valkey==6.1.1 # via -r requirements/lint.in -virtualenv==21.1.0 +virtualenv==21.3.1 # via pre-commit wait-for-it==2.3.0 # via -r requirements/test-common.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 7942783e26f..794cfd3d493 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -192,7 +192,7 @@ pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 # via freezegun -python-discovery==1.1.0 +python-discovery==1.3.0 # via virtualenv python-on-whales==0.80.0 # via @@ -275,7 +275,7 @@ uvloop==0.21.0 ; platform_system != "Windows" and implementation_name == "cpytho # -r requirements/lint.in valkey==6.1.1 # via -r requirements/lint.in -virtualenv==21.1.0 +virtualenv==21.3.1 # via pre-commit wait-for-it==2.3.0 # via -r requirements/test-common.in diff --git a/requirements/lint.txt b/requirements/lint.txt index a59e7dcbb60..410be497228 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -92,7 +92,7 @@ pytest-mock==3.15.1 # via -r requirements/lint.in python-dateutil==2.9.0.post0 # via freezegun -python-discovery==1.1.0 +python-discovery==1.3.0 # via virtualenv python-on-whales==0.80.0 # via -r requirements/lint.in @@ -127,7 +127,7 @@ uvloop==0.21.0 ; platform_system != "Windows" # via -r requirements/lint.in valkey==6.1.1 # via -r requirements/lint.in -virtualenv==21.1.0 +virtualenv==21.3.1 # via pre-commit zlib-ng==1.0.0 # via -r requirements/lint.in From 2372a0be1e4ddbdd840846760e1aa1d62042b809 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 11:51:26 +0000 Subject: [PATCH 045/191] Bump setuptools from 82.0.0 to 82.0.1 (#12470) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [setuptools](https://github.com/pypa/setuptools) from 82.0.0 to 82.0.1.
Changelog

Sourced from setuptools's changelog.

v82.0.1

Bugfixes

  • Fix the loading of launcher manifest.xml file. (#5047)
  • Replaced deprecated json.__version__ with fixture in tests. (#5186)

Improved Documentation

  • Add advice about how to improve predictability when installing sdists. (#5168)

Misc

Commits
  • 5a13876 Bump version: 82.0.0 → 82.0.1
  • 51ab8f1 Avoid using (deprecated) 'json.version' in tests (#5194)
  • f9c37b2 Docs/CI: Fix intersphinx references (#5195)
  • 8173db2 Docs: Fix intersphinx references
  • 09bafbc Fix past tense on newsfragment
  • 461ea56 Add news fragment
  • c4ffe53 Avoid using (deprecated) 'json.version' in tests
  • 749258b Cleanup pkg_resources dependencies and configuration (#5175)
  • 2019c16 Parse ext-module.define-macros from pyproject.toml as list of tuples (#5169)
  • b809c86 Sync setuptools schema with validate-pyproject (#5157)
  • 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=82.0.0&new-version=82.0.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 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 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index f5575bf5e25..535610671a7 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -301,5 +301,5 @@ zlib-ng==1.0.0 # The following packages are considered to be unsafe in a requirements file: pip==26.0.1 # via pip-tools -setuptools==82.0.0 +setuptools==82.0.1 # via pip-tools diff --git a/requirements/dev.txt b/requirements/dev.txt index 794cfd3d493..c75165bee76 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -291,5 +291,5 @@ zlib-ng==1.0.0 # The following packages are considered to be unsafe in a requirements file: pip==26.0.1 # via pip-tools -setuptools==82.0.0 +setuptools==82.0.1 # via pip-tools From 8b3a6085b1bcc531075da3ae7ad140ead8dbff00 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 12:08:54 +0000 Subject: [PATCH 046/191] Bump pygments from 2.19.2 to 2.20.0 (#12475) Bumps [pygments](https://github.com/pygments/pygments) from 2.19.2 to 2.20.0.
Release notes

Sourced from pygments's releases.

2.20.0

  • New lexers:

  • Updated lexers:

    • archetype: Fix catastrophic backtracking in GUID and ID patterns (#3064)
    • ASN.1: Recognize minus sign and fix range operator (#3014, #3060)
    • C++: Add C++26 keywords (#2955), add integer literal suffixes (#2966)
    • ComponentPascal: Fix analyse_text (#3028, #3032)
    • Coq renamed to Rocq (#2883, #2908)
    • Cython: Various improvements (#2932, #2933)
    • Debian control: Improve architecture parsing (#3052)
    • Devicetree: Add support for overlay/fragments (#3021), add bytestring support (#3022), fix catastrophic backtracking (#3057)
    • Fennel: Various improvements (#2911)
    • Haskell: Handle escape sequences in character literals (#3069, #1795)
    • Java: Add module keywords (#2955)
    • Lean4: Add operators ]', ]?, ]! (#2946)
    • LESS: Support single-line comments (#3005)
    • LilyPond: Update to 2.25.29 (#2974)
    • LLVM: Support C-style comments (#3023, #2978)
    • Lua(u): Fix catastrophic backtracking (#3047)
    • Macaulay2: Update to 1.25.05 (#2893), 1.25.11 (#2988)
    • Mathematica: Various improvements (#2957)
    • meson: Add additional operators (#2919)
    • MySQL: Update keywords (#2970)
    • org-Mode: Support both schedule and deadline (#2899)
    • PHP: Add __PROPERTY__ magic constant (#2924), add reserved keywords (#3002)
    • PostgreSQL: Add more keywords (#2985)
    • protobuf: Fix namespace tokenization (#2929)
    • Python: Add t-string support (#2973, #3009, #3010)
    • Tablegen: Fix infinite loop (#2972, #2940)
    • Tera Term macro: Add commands introduced in v5.3 through v5.6 (#2951)
    • TOML: Support TOML 1.1.0 (#3026, #3027)
    • Turtle: Allow empty comment lines (#2980)
    • XML: Added .xbrl as file ending (#2890, #2891)
  • Drop Python 3.8, and add Python 3.14 as a supported version (#2987, #3012)

  • Various improvements to autopygmentize (#2894)

  • Update onedark style to support more token types (#2977)

  • Update rtt style to support more token types (#2895)

  • Cache entry points to improve performance (#2979)

  • Fix xterm-256 color table (#3043)

  • Fix kwargs dictionary getting mutated on each call (#3044)

Changelog

Sourced from pygments's changelog.

Version 2.20.0

(released March 29th, 2026)

  • New lexers:

  • Updated lexers:

    • archetype: Fix catastrophic backtracking in GUID and ID patterns (#3064)
    • ASN.1: Recognize minus sign and fix range operator (#3014, #3060)
    • C++: Add C++26 keywords (#2955), add integer literal suffixes (#2966)
    • ComponentPascal: Fix analyse_text (#3028, #3032)
    • Coq renamed to Rocq (#2883, #2908)
    • Cython: Various improvements (#2932, #2933)
    • Debian control: Improve architecture parsing (#3052)
    • Devicetree: Add support for overlay/fragments (#3021), add bytestring support (#3022), fix catastrophic backtracking (#3057)
    • Fennel: Various improvements (#2911)
    • Haskell: Handle escape sequences in character literals (#3069, #1795)
    • Java: Add module keywords (#2955)
    • Lean4: Add operators ]', ]?, ]! (#2946)
    • LESS: Support single-line comments (#3005)
    • LilyPond: Update to 2.25.29 (#2974)
    • LLVM: Support C-style comments (#3023, #2978)
    • Lua(u): Fix catastrophic backtracking (#3047)
    • Macaulay2: Update to 1.25.05 (#2893), 1.25.11 (#2988)
    • Mathematica: Various improvements (#2957)
    • meson: Add additional operators (#2919)
    • MySQL: Update keywords (#2970)
    • org-Mode: Support both schedule and deadline (#2899)
    • PHP: Add __PROPERTY__ magic constant (#2924), add reserved keywords (#3002)
    • PostgreSQL: Add more keywords (#2985)
    • protobuf: Fix namespace tokenization (#2929)
    • Python: Add t-string support (#2973, #3009, #3010)
    • Tablegen: Fix infinite loop (#2972, #2940)
    • Tera Term macro: Add commands introduced in v5.3 through v5.6 (#2951)
    • TOML: Support TOML 1.1.0 (#3026, #3027)
    • Turtle: Allow empty comment lines (#2980)
    • XML: Added .xbrl as file ending (#2890, #2891)
  • Drop Python 3.8, and add Python 3.14 as a supported version (#2987, #3012)

  • Various improvements to autopygmentize (#2894)

  • Update onedark style to support more token types (#2977)

  • Update rtt style to support more token types (#2895)

  • Cache entry points to improve performance (#2979)

  • Fix xterm-256 color table (#3043)

  • Fix kwargs dictionary getting mutated on each call (#3044)

Commits
  • 708197d Fix underline length.
  • 1d4538a Prepare 2.20 release.
  • 2ceaee4 Update CHANGES.
  • e3a3c54 Fix Haskell lexer: handle escape sequences in character literals (#3069)
  • d7c3453 Merge pull request #3071 from pygments/harden-html-formatter
  • 0f97e7c Harden the HTML formatter against CSS.
  • 9f981b2 Update CHANGES.
  • 1d88915 Update CHANGES.
  • c3d93ad Fix ASN.1 lexer: recognize minus sign and fix range operator (#3060)
  • 4f06bcf fix bad behaving backtracking regex in CommonLispLexer
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pygments&package-manager=pip&previous-version=2.19.2&new-version=2.20.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 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 +- requirements/lint.txt | 2 +- requirements/test-common.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 535610671a7..2c81c25bd66 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -166,7 +166,7 @@ pydantic-core==2.46.3 # via pydantic pyenchant==3.3.0 # via sphinxcontrib-spelling -pygments==2.19.2 +pygments==2.20.0 # via # pytest # rich diff --git a/requirements/dev.txt b/requirements/dev.txt index c75165bee76..61a4ae0962f 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -161,7 +161,7 @@ pydantic==2.13.3 # via python-on-whales pydantic-core==2.46.3 # via pydantic -pygments==2.19.2 +pygments==2.20.0 # via # pytest # rich diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index 080bbd7f499..a95f384fc40 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -32,7 +32,7 @@ packaging==26.2 # via sphinx pyenchant==3.3.0 # via sphinxcontrib-spelling -pygments==2.19.2 +pygments==2.20.0 # via sphinx requests==2.33.1 # via diff --git a/requirements/doc.txt b/requirements/doc.txt index b52661badb7..aec4d0e8e80 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -30,7 +30,7 @@ markupsafe==3.0.3 # via jinja2 packaging==26.2 # via sphinx -pygments==2.19.2 +pygments==2.20.0 # via sphinx requests==2.33.1 # via sphinx diff --git a/requirements/lint.txt b/requirements/lint.txt index 410be497228..7fb30b7affa 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -77,7 +77,7 @@ pydantic==2.13.3 # via python-on-whales pydantic-core==2.46.3 # via pydantic -pygments==2.19.2 +pygments==2.20.0 # via # pytest # rich diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 28caba8dc46..b9bf8359dc1 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -62,7 +62,7 @@ pydantic==2.13.3 # via python-on-whales pydantic-core==2.46.3 # via pydantic -pygments==2.19.2 +pygments==2.20.0 # via # pytest # rich diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 77bef1382ed..7be26f35a61 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -97,7 +97,7 @@ pydantic==2.13.3 # via python-on-whales pydantic-core==2.46.3 # via pydantic -pygments==2.19.2 +pygments==2.20.0 # via # pytest # rich diff --git a/requirements/test.txt b/requirements/test.txt index a2533087c0e..89f62f3d281 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -97,7 +97,7 @@ pydantic==2.13.3 # via python-on-whales pydantic-core==2.46.3 # via pydantic -pygments==2.19.2 +pygments==2.20.0 # via # pytest # rich From be99ca1c406d9284b62da59769292aed9d913eeb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 12:10:42 +0000 Subject: [PATCH 047/191] Bump tomli from 2.4.0 to 2.4.1 (#12473) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [tomli](https://github.com/hukkin/tomli) from 2.4.0 to 2.4.1.
Changelog

Sourced from tomli's changelog.

2.4.1

  • Fixed
    • Limit number of parts of a TOML key to address quadratic time complexity
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tomli&package-manager=pip&previous-version=2.4.0&new-version=2.4.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 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 +- requirements/lint.txt | 2 +- requirements/test-common.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 2c81c25bd66..74a43b6df33 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -244,7 +244,7 @@ sphinxcontrib-spelling==8.0.2 ; platform_system != "Windows" # via -r requirements/doc-spelling.in sphinxcontrib-towncrier==0.5.0a0 # via -r requirements/doc.in -tomli==2.4.0 +tomli==2.4.1 # via # build # coverage diff --git a/requirements/dev.txt b/requirements/dev.txt index 61a4ae0962f..31255ba538a 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -234,7 +234,7 @@ sphinxcontrib-serializinghtml==2.0.0 # via sphinx sphinxcontrib-towncrier==0.5.0a0 # via -r requirements/doc.in -tomli==2.4.0 +tomli==2.4.1 # via # build # coverage diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index a95f384fc40..f64de280c8c 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -61,7 +61,7 @@ sphinxcontrib-spelling==8.0.2 ; platform_system != "Windows" # via -r requirements/doc-spelling.in sphinxcontrib-towncrier==0.5.0a0 # via -r requirements/doc.in -tomli==2.4.0 +tomli==2.4.1 # via # sphinx # towncrier diff --git a/requirements/doc.txt b/requirements/doc.txt index aec4d0e8e80..e09ceb721a3 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -54,7 +54,7 @@ sphinxcontrib-serializinghtml==2.0.0 # via sphinx sphinxcontrib-towncrier==0.5.0a0 # via -r requirements/doc.in -tomli==2.4.0 +tomli==2.4.1 # via # sphinx # towncrier diff --git a/requirements/lint.txt b/requirements/lint.txt index 7fb30b7affa..75cfe9fe809 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -104,7 +104,7 @@ six==1.17.0 # via python-dateutil slotscheck==0.19.1 # via -r requirements/lint.in -tomli==2.4.0 +tomli==2.4.1 # via # mypy # pytest diff --git a/requirements/test-common.txt b/requirements/test-common.txt index b9bf8359dc1..9fb6a8a376c 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -95,7 +95,7 @@ setuptools-git==1.2 # via -r requirements/test-common.in six==1.17.0 # via python-dateutil -tomli==2.4.0 +tomli==2.4.1 # via # coverage # mypy diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 7be26f35a61..e09a0ec7a1e 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -130,7 +130,7 @@ setuptools-git==1.2 # via -r requirements/test-common.in six==1.17.0 # via python-dateutil -tomli==2.4.0 +tomli==2.4.1 # via # coverage # mypy diff --git a/requirements/test.txt b/requirements/test.txt index 89f62f3d281..7538efa214d 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -130,7 +130,7 @@ setuptools-git==1.2 # via -r requirements/test-common.in six==1.17.0 # via python-dateutil -tomli==2.4.0 +tomli==2.4.1 # via # coverage # mypy From 6037db9984f5d398faba1e06fd2a389f0d31aa14 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 12:40:24 +0000 Subject: [PATCH 048/191] Bump pytest from 9.0.2 to 9.0.3 (#12474) Bumps [pytest](https://github.com/pytest-dev/pytest) from 9.0.2 to 9.0.3.
Release notes

Sourced from pytest's releases.

9.0.3

pytest 9.0.3 (2026-04-07)

Bug fixes

  • #12444: Fixed pytest.approx which now correctly takes into account ~collections.abc.Mapping keys order to compare them.

  • #13634: Blocking a conftest.py file using the -p no: option is now explicitly disallowed.

    Previously this resulted in an internal assertion failure during plugin loading.

    Pytest now raises a clear UsageError explaining that conftest files are not plugins and cannot be disabled via -p.

  • #13734: Fixed crash when a test raises an exceptiongroup with __tracebackhide__ = True.

  • #14195: Fixed an issue where non-string messages passed to unittest.TestCase.subTest() were not printed.

  • #14343: Fixed use of insecure temporary directory (CVE-2025-71176).

Improved documentation

  • #13388: Clarified documentation for -p vs PYTEST_PLUGINS plugin loading and fixed an incorrect -p example.
  • #13731: Clarified that capture fixtures (e.g. capsys and capfd) take precedence over the -s / --capture=no command-line options in Accessing captured output from a test function <accessing-captured-output>.
  • #14088: Clarified that the default pytest_collection hook sets session.items before it calls pytest_collection_finish, not after.
  • #14255: TOML integer log levels must be quoted: Updating reference documentation.

Contributor-facing changes

  • #12689: The test reports are now published to Codecov from GitHub Actions. The test statistics is visible on the web interface.

    -- by aleguy02

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 74a43b6df33..edbc63e26fb 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -175,7 +175,7 @@ pyproject-hooks==1.2.0 # via # build # pip-tools -pytest==9.0.2 +pytest==9.0.3 # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 31255ba538a..89c2d73a16e 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -170,7 +170,7 @@ pyproject-hooks==1.2.0 # via # build # pip-tools -pytest==9.0.2 +pytest==9.0.3 # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/lint.txt b/requirements/lint.txt index 75cfe9fe809..39d3dfaec35 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -81,7 +81,7 @@ pygments==2.20.0 # via # pytest # rich -pytest==9.0.2 +pytest==9.0.3 # via # -r requirements/lint.in # pytest-codspeed diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 9fb6a8a376c..88f7b23268a 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -66,7 +66,7 @@ pygments==2.20.0 # via # pytest # rich -pytest==9.0.2 +pytest==9.0.3 # via # -r requirements/test-common.in # pytest-codspeed diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index e09a0ec7a1e..9ec4732babd 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -101,7 +101,7 @@ pygments==2.20.0 # via # pytest # rich -pytest==9.0.2 +pytest==9.0.3 # via # -r requirements/test-common.in # pytest-codspeed diff --git a/requirements/test.txt b/requirements/test.txt index 7538efa214d..0cbfe9463f9 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -101,7 +101,7 @@ pygments==2.20.0 # via # pytest # rich -pytest==9.0.2 +pytest==9.0.3 # via # -r requirements/test-common.in # pytest-codspeed From 77872abd115b79a2be9ff97cebb504ebf43d5df0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 11:43:32 +0000 Subject: [PATCH 049/191] Bump wheel from 0.46.3 to 0.47.0 (#12483) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [wheel](https://github.com/pypa/wheel) from 0.46.3 to 0.47.0.
Release notes

Sourced from wheel's releases.

0.47.0

  • Added the wheel info subcommand to display metadata about wheel files without unpacking them (#639)
  • Fixed WheelFile raising Missing RECORD file when the wheel filename contains uppercase characters (e.g. Django-3.2.5.whl) but the .dist-info directory inside uses normalized lowercase naming (#411)
Changelog

Sourced from wheel's changelog.

Release Notes

0.47.0 (2026-04-22)

  • Added the wheel info subcommand to display metadata about wheel files without unpacking them ([#639](https://github.com/pypa/wheel/issues/639) <https://github.com/pypa/wheel/issues/639>_)
  • Fixed WheelFile raising Missing RECORD file when the wheel filename contains uppercase characters (e.g. Django-3.2.5.whl) but the .dist-info directory inside uses normalized lowercase naming ([#411](https://github.com/pypa/wheel/issues/411) <https://github.com/pypa/wheel/issues/411>_)

0.46.3 (2026-01-22)

  • Fixed ImportError: cannot import name '_setuptools_logging' from 'wheel' when installed alongside an old version of setuptools and running the bdist_wheel command ([#676](https://github.com/pypa/wheel/issues/676) <https://github.com/pypa/wheel/issues/676>_)

0.46.2 (2026-01-22)

  • Restored the bdist_wheel command for compatibility with setuptools older than v70.1
  • Importing wheel.bdist_wheel now emits a FutureWarning instead of a DeprecationWarning
  • Fixed wheel unpack potentially altering the permissions of files outside of the destination tree with maliciously crafted wheels (CVE-2026-24049)

0.46.1 (2025-04-08)

  • Temporarily restored the wheel.macosx_libfile module ([#659](https://github.com/pypa/wheel/issues/659) <https://github.com/pypa/wheel/issues/659>_)

0.46.0 (2025-04-03)

  • Dropped support for Python 3.8
  • Removed the bdist_wheel setuptools command implementation and entry point. The wheel.bdist_wheel module is now just an alias to setuptools.command.bdist_wheel, emitting a deprecation warning on import.
  • Removed vendored packaging in favor of a run-time dependency on it
  • Made the wheel.metadata module private (with a deprecation warning if it's imported
  • Made the wheel.cli package private (no deprecation warning)
  • Fixed an exception when calling the convert command with an empty description field

0.45.1 (2024-11-23)

  • Fixed pure Python wheels converted from eggs and wininst files having the ABI tag in the file name

... (truncated)

Commits
  • efd83a7 Created a new release
  • bb69216 Reordered the changelog entries
  • d5a1763 fix(wheelfile): resolve .dist-info path case-insensitively when reading wheel...
  • 5718957 [pre-commit.ci] pre-commit autoupdate (#685)
  • 6258068 chore: log_level is better than log_cli_level (#684)
  • 2975deb Require tox >= 4.22
  • 47674ba chore: add check-sdist to checks (#681)
  • 56223f6 __package____spec__.parent (#679)
  • 0ce509e Added the wheel info subcommand (#669)
  • 39039c0 Improved the index page
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=wheel&package-manager=pip&previous-version=0.46.3&new-version=0.47.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 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 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index edbc63e26fb..5413fdc353d 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -289,7 +289,7 @@ virtualenv==21.3.1 # via pre-commit wait-for-it==2.3.0 # via -r requirements/test-common.in -wheel==0.46.3 +wheel==0.47.0 # via pip-tools yarl==1.22.0 # via -r requirements/runtime-deps.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 89c2d73a16e..c961fafe55c 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -279,7 +279,7 @@ virtualenv==21.3.1 # via pre-commit wait-for-it==2.3.0 # via -r requirements/test-common.in -wheel==0.46.3 +wheel==0.47.0 # via pip-tools yarl==1.22.0 # via -r requirements/runtime-deps.in From e1dc6655f25de08815515355d77aca629580e4de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 11:45:47 +0000 Subject: [PATCH 050/191] Bump librt from 0.9.0 to 0.10.0 (#12484) Bumps [librt](https://github.com/mypyc/librt) from 0.9.0 to 0.10.0.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=librt&package-manager=pip&previous-version=0.9.0&new-version=0.10.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 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-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 5413fdc353d..b2930c47173 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -106,7 +106,7 @@ jinja2==3.1.6 # via # sphinx # towncrier -librt==0.9.0 +librt==0.10.0 # via mypy markdown-it-py==4.0.0 # via rich diff --git a/requirements/dev.txt b/requirements/dev.txt index c961fafe55c..a787c04fdaa 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -104,7 +104,7 @@ jinja2==3.1.6 # via # sphinx # towncrier -librt==0.9.0 +librt==0.10.0 # via mypy markdown-it-py==4.0.0 # via rich diff --git a/requirements/lint.txt b/requirements/lint.txt index 39d3dfaec35..56b407e0345 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -45,7 +45,7 @@ iniconfig==2.3.0 # via pytest isal==1.7.2 # via -r requirements/lint.in -librt==0.9.0 +librt==0.10.0 # via mypy markdown-it-py==4.0.0 # via rich diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 88f7b23268a..e2519fd4a6b 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -34,7 +34,7 @@ iniconfig==2.3.0 # via pytest isal==1.8.0 ; python_version < "3.14" # via -r requirements/test-common.in -librt==0.9.0 +librt==0.10.0 # via mypy markdown-it-py==4.0.0 # via rich diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 9ec4732babd..b6299d50600 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -57,7 +57,7 @@ iniconfig==2.3.0 # via pytest isal==1.8.0 ; python_version < "3.14" # via -r requirements/test-common.in -librt==0.9.0 +librt==0.10.0 # via mypy markdown-it-py==4.0.0 # via rich diff --git a/requirements/test.txt b/requirements/test.txt index 0cbfe9463f9..623135eb77a 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -57,7 +57,7 @@ iniconfig==2.3.0 # via pytest isal==1.7.2 ; python_version < "3.14" # via -r requirements/test-common.in -librt==0.9.0 +librt==0.10.0 # via mypy markdown-it-py==4.0.0 # via rich From 5ee867ad78a469ebe5922dc7811bb7cbbcf55f9c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 12:04:40 +0000 Subject: [PATCH 051/191] Bump pip from 26.0.1 to 26.1.1 (#12481) Bumps [pip](https://github.com/pypa/pip) from 26.0.1 to 26.1.1.
Changelog

Sourced from pip's changelog.

26.1.1 (2026-05-04)

Bug Fixes

  • Fix issue where uninstallation left behind empty directories. Revert the removal of the adjacent __pycache__ directory when a .py file is removed. ([#13973](https://github.com/pypa/pip/issues/13973) <https://github.com/pypa/pip/issues/13973>_)

26.1 (2026-04-26)

Deprecations and Removals

  • Drop support for Python 3.9. ([#13795](https://github.com/pypa/pip/issues/13795) <https://github.com/pypa/pip/issues/13795>_)

Features

  • Add experimental support to read requirements from standardized pylock.toml files (-r pylock.toml). ([#13876](https://github.com/pypa/pip/issues/13876) <https://github.com/pypa/pip/issues/13876>_)
  • Allow --uploaded-prior-to to accept a duration in days (e.g., P3D for 3 days ago). ([#13674](https://github.com/pypa/pip/issues/13674) <https://github.com/pypa/pip/issues/13674>_)

Enhancements

  • Speed up dependency resolution when there are complex conflicts. ([#13859](https://github.com/pypa/pip/issues/13859) <https://github.com/pypa/pip/issues/13859>_)
  • Reduce memory usage when resolving large dependency trees. ([#13843](https://github.com/pypa/pip/issues/13843) <https://github.com/pypa/pip/issues/13843>_)
  • Emit a deprecation warning when pip imports an unexpected module after installation of a distribution has started. ([#13912](https://github.com/pypa/pip/issues/13912) <https://github.com/pypa/pip/issues/13912>_)
  • Allow URL constraints to apply to requirements with extras. ([#12018](https://github.com/pypa/pip/issues/12018) <https://github.com/pypa/pip/issues/12018>_)
  • Allow unpinned requirements to use hashes from constraints. Constraints like {name}=={version} --hash=... feeds into hash verification for a corresponding requirement. ([#9243](https://github.com/pypa/pip/issues/9243) <https://github.com/pypa/pip/issues/9243>_)
  • Improve conflict reports that involve direct URLs. ([#13932](https://github.com/pypa/pip/issues/13932) <https://github.com/pypa/pip/issues/13932>_)
  • Show all errors instead of first error for faulty dependency_groups definitions. ([#13917](https://github.com/pypa/pip/issues/13917) <https://github.com/pypa/pip/issues/13917>_)

Bug Fixes

  • Fix recovery hint for missing RECORD file to use --ignore-installed instead of --force-reinstall. ([#12645](https://github.com/pypa/pip/issues/12645) <https://github.com/pypa/pip/issues/12645>_)
  • Fix misleading error message when a constraint file cannot be opened. ([#13226](https://github.com/pypa/pip/issues/13226) <https://github.com/pypa/pip/issues/13226>_)
  • Show the filename rather than the full URL when downloading files from non-PyPI indexes in non-verbose mode. ([#13494](https://github.com/pypa/pip/issues/13494) <https://github.com/pypa/pip/issues/13494>_)
  • Remove the adjacent __pycache__ directory when a .py file is removed. ([#13725](https://github.com/pypa/pip/issues/13725) <https://github.com/pypa/pip/issues/13725>_)
  • Force UTF-8 encoding for :pep:723 metadata. ([#13861](https://github.com/pypa/pip/issues/13861) <https://github.com/pypa/pip/issues/13861>_)
  • Minor performance improvement when filtering candidates during resolution. ([#13916](https://github.com/pypa/pip/issues/13916) <https://github.com/pypa/pip/issues/13916>_)
  • Fix a hang on Windows when stdout is closed during verbose output. ([#13927](https://github.com/pypa/pip/issues/13927) <https://github.com/pypa/pip/issues/13927>_)
  • Common path prefixes are determined by path segment, not character by character. ([#13847](https://github.com/pypa/pip/issues/13847) <https://github.com/pypa/pip/issues/13847>_)
  • Fix installing .tar.gz source distributions that look like a zip file. ([#13867](https://github.com/pypa/pip/issues/13867) <https://github.com/pypa/pip/issues/13867>_)

... (truncated)

Commits
  • 4432a37 Bump for release
  • 4943e17 Merge pull request #13973 from pypa/revert-13725-vfazio-remove-all-optimizati...
  • e9e7b90 Add news
  • 0ff6964 Revert "Remove pycache when package is removed"
  • cc6b082 Merge pull request #13951 from sbidoul/release/26.1
  • b2671f1 Bump for development
  • 90b2b3e Bump for release
  • 193f289 Update AUTHORS.txt
  • 63c3709 Merge pull request #13876 from sbidoul/install-from-pylock-reqs-sbi
  • e5fe702 Merge pull request #13949 from pypa/revert-13888-resolver-editable-links
  • Additional commits viewable in compare view

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 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index b2930c47173..7353c035354 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -299,7 +299,7 @@ zlib-ng==1.0.0 # -r requirements/test-common.in # The following packages are considered to be unsafe in a requirements file: -pip==26.0.1 +pip==26.1.1 # via pip-tools setuptools==82.0.1 # via pip-tools diff --git a/requirements/dev.txt b/requirements/dev.txt index a787c04fdaa..8355ced4e2d 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -289,7 +289,7 @@ zlib-ng==1.0.0 # -r requirements/test-common.in # The following packages are considered to be unsafe in a requirements file: -pip==26.0.1 +pip==26.1.1 # via pip-tools setuptools==82.0.1 # via pip-tools From e6938c82850fde062bba092c0447e401cab729fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 11:08:45 +0000 Subject: [PATCH 052/191] Bump urllib3 from 2.6.3 to 2.7.0 (#12488) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.3 to 2.7.0.
Release notes

Sourced from urllib3's releases.

2.7.0

🚀 urllib3 is fundraising for HTTP/2 support

urllib3 is raising ~$40,000 USD to release HTTP/2 support and ensure long-term sustainable maintenance of the project after a sharp decline in financial support. If your company or organization uses Python and would benefit from HTTP/2 support in Requests, pip, cloud SDKs, and thousands of other projects please consider contributing financially to ensure HTTP/2 support is developed sustainably and maintained for the long-haul.

Thank you for your support.

Security

Addressed high-severity security issues. Impact was limited to specific use cases detailed in the accompanying advisories; overall user exposure was estimated to be marginal.

  • Decompression-bomb safeguards of the streaming API were bypassed:

    1. When HTTPResponse.drain_conn() was called after the response had been read and decompressed partially. (Reported by @​Cycloctane)
    2. During the second HTTPResponse.read(amt=N) or HTTPResponse.stream(amt=N) call when the response was decompressed using the official Brotli library. (Reported by @​kimkou2024)

    See GHSA-mf9v-mfxr-j63j for details.

  • HTTP pools created using ProxyManager.connection_from_url did not strip sensitive headers specified in Retry.remove_headers_on_redirect when redirecting to a different host. (GHSA-qccp-gfcp-xxvc reported by @​christos-spearbit)

Deprecations and Removals

  • Used FutureWarning instead of DeprecationWarning for better visibility of existing deprecation notices. Rescheduled the removal of deprecated features to version 3.0. (urllib3/urllib3#3764)
  • Removed support for end-of-life Python 3.9. (urllib3/urllib3#3720)
  • Removed support for end-of-life PyPy3.10. (urllib3/urllib3#4979)
  • Bumped the minimum supported pyOpenSSL version to 19.0.0. (urllib3/urllib3#3777)

Bugfixes

  • Fixed a bug where HTTPResponse.read(amt=None) was ignoring decompressed data buffered from previous partial reads. (urllib3/urllib3#3636)
  • Fixed a bug where HTTPResponse.read() could cache only part of the response after a partial read when cache_content=True. (urllib3/urllib3#4967)
  • Fixed HTTPResponse.stream() and HTTPResponse.read_chunked() to handle amt=0. (urllib3/urllib3#3793)
  • Updated _TYPE_BODY type alias to include missing Iterable[str], matching the documented and runtime behavior of chunked request bodies. (urllib3/urllib3#3798)
  • Fixed LocationParseError when paths resembling schemeless URIs were passed to HTTPConnectionPool.urlopen(). (urllib3/urllib3#3352)
  • Fixed BaseHTTPResponse.readinto() type annotation to accept memoryview in addition to bytearray, matching the io.RawIOBase.readinto contract and enabling use with io.BufferedReader without type errors. (urllib3/urllib3#3764)
Changelog

Sourced from urllib3's changelog.

2.7.0 (2026-05-07)

Security

Addressed high-severity security issues. Impact was limited to specific use cases detailed in the accompanying advisories; overall user exposure was estimated to be marginal.

  • Decompression-bomb safeguards of the streaming API were bypassed:

    1. When HTTPResponse.drain_conn() was called after the response had been read and decompressed partially.
    2. During the second HTTPResponse.read(amt=N) or HTTPResponse.stream(amt=N) call when the response was decompressed using the official Brotli <https://pypi.org/project/brotli/>__ library.

    See GHSA-mf9v-mfxr-j63j <https://github.com/urllib3/urllib3/security/advisories/GHSA-mf9v-mfxr-j63j>__ for details.

  • HTTP pools created using ProxyManager.connection_from_url did not strip sensitive headers specified in Retry.remove_headers_on_redirect when redirecting to a different host. (GHSA-qccp-gfcp-xxvc <https://github.com/urllib3/urllib3/security/advisories/GHSA-qccp-gfcp-xxvc>__)

Deprecations and Removals

  • Used FutureWarning instead of DeprecationWarning for better visibility of existing deprecation notices. Rescheduled the removal of deprecated features to version 3.0. ([#3764](https://github.com/urllib3/urllib3/issues/3764) <https://github.com/urllib3/urllib3/issues/3764>__)
  • Removed support for end-of-life Python 3.9. ([#3720](https://github.com/urllib3/urllib3/issues/3720) <https://github.com/urllib3/urllib3/issues/3720>__)
  • Removed support for end-of-life PyPy3.10. ([#4979](https://github.com/urllib3/urllib3/issues/4979) <https://github.com/urllib3/urllib3/issues/4979>__)
  • Bumped the minimum supported pyOpenSSL version to 19.0.0. ([#3777](https://github.com/urllib3/urllib3/issues/3777) <https://github.com/urllib3/urllib3/issues/3777>__)

Bugfixes

  • Fixed a bug where HTTPResponse.read(amt=None) was ignoring decompressed data buffered from previous partial reads. ([#3636](https://github.com/urllib3/urllib3/issues/3636) <https://github.com/urllib3/urllib3/issues/3636>__)
  • Fixed a bug where HTTPResponse.read() could cache only part of the response after a partial read when cache_content=True.

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=urllib3&package-manager=pip&previous-version=2.6.3&new-version=2.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 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 7353c035354..3f591426905 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -277,7 +277,7 @@ typing-extensions==4.15.0 ; python_version < "3.13" # virtualenv typing-inspection==0.4.2 # via pydantic -urllib3==2.6.3 +urllib3==2.7.0 # via requests uvloop==0.21.0 ; platform_system != "Windows" # via diff --git a/requirements/dev.txt b/requirements/dev.txt index 8355ced4e2d..39b38894931 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -267,7 +267,7 @@ typing-extensions==4.15.0 ; python_version < "3.13" # virtualenv typing-inspection==0.4.2 # via pydantic -urllib3==2.6.3 +urllib3==2.7.0 # via requests uvloop==0.21.0 ; platform_system != "Windows" and implementation_name == "cpython" # via diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index f64de280c8c..88eea72170c 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -69,5 +69,5 @@ towncrier==25.8.0 # via # -r requirements/doc.in # sphinxcontrib-towncrier -urllib3==2.6.3 +urllib3==2.7.0 # via requests diff --git a/requirements/doc.txt b/requirements/doc.txt index e09ceb721a3..4cff982765e 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -62,5 +62,5 @@ towncrier==25.8.0 # via # -r requirements/doc.in # sphinxcontrib-towncrier -urllib3==2.6.3 +urllib3==2.7.0 # via requests From 1cddba9efff057866872d017a6d1f7ef634bab65 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 11:12:34 +0000 Subject: [PATCH 053/191] Bump filelock from 3.25.0 to 3.29.0 (#12487) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.25.0 to 3.29.0.
Release notes

Sourced from filelock's releases.

3.29.0

What's Changed

Full Changelog: https://github.com/tox-dev/filelock/compare/3.28.0...3.29.0

3.28.0

What's Changed

Full Changelog: https://github.com/tox-dev/filelock/compare/3.27.0...3.28.0

3.27.0

What's Changed

Full Changelog: https://github.com/tox-dev/filelock/compare/3.26.1...3.27.0

3.26.1

What's Changed

New Contributors

Full Changelog: https://github.com/tox-dev/filelock/compare/3.26.0...3.26.1

3.26.0

What's Changed

Full Changelog: https://github.com/tox-dev/filelock/compare/3.25.2...3.26.0

... (truncated)

Changelog

Sourced from filelock's changelog.

########### Changelog ###########


3.29.0 (2026-04-19)


  • ✨ feat(soft): enable stale lock detection on Windows :pr:534
  • 🐛 fix(async): use single-thread executor for lock consistency :pr:533
  • build(deps): bump actions/upload-artifact from 7.0.0 to 7.0.1 :pr:530 - by :user:dependabot[bot]

3.28.0 (2026-04-14)


  • 🐛 fix(ci): unbreak release workflow, publish to PyPI again :pr:529

3.26.1 (2026-04-09)


  • 🐛 fix(asyncio): add exit to BaseAsyncFileLock and fix del loop handling :pr:518 - by :user:naarob
  • build(deps): bump pypa/gh-action-pypi-publish from 1.13.0 to 1.14.0 :pr:525 - by :user:dependabot[bot]

3.26.0 (2026-04-06)


  • ✨ feat(soft): add PID inspection and lock breaking :pr:524
  • [pre-commit.ci] pre-commit autoupdate :pr:523 - by :user:pre-commit-ci[bot]
  • build(deps): bump astral-sh/setup-uv from 7.6.0 to 8.0.0 :pr:522 - by :user:dependabot[bot]
  • Remove persist-credentials: false from release job :pr:520
  • [pre-commit.ci] pre-commit autoupdate :pr:519 - by :user:pre-commit-ci[bot]
  • 🔒 ci(workflows): add zizmor security auditing :pr:517
  • [pre-commit.ci] pre-commit autoupdate :pr:516 - by :user:pre-commit-ci[bot]
  • [pre-commit.ci] pre-commit autoupdate :pr:514 - by :user:pre-commit-ci[bot]

3.25.2 (2026-03-11)


  • 🐛 fix(unix): suppress EIO on close in Docker bind mounts :pr:513

3.25.1 (2026-03-09)


  • [pre-commit.ci] pre-commit autoupdate :pr:510 - by :user:pre-commit-ci[bot]
  • 🐛 fix(win): restore best-effort lock file cleanup on release :pr:511

... (truncated)

Commits
  • 469b47f Release 3.29.0
  • e85d072 ✨ feat(soft): enable stale lock detection on Windows (#534)
  • f5ee171 🐛 fix(async): use single-thread executor for lock consistency (#533)
  • 2a95458 build(deps): bump actions/upload-artifact from 7.0.0 to 7.0.1 (#530)
  • 55de20c Release 3.28.0
  • 476b0e4 🐛 fix(ci): unbreak release workflow, publish to PyPI again (#529)
  • 824713e ✨ feat(rw): add SoftReadWriteLock for NFS and HPC clusters (#528)
  • 9879de9 [pre-commit.ci] pre-commit autoupdate (#527)
  • 4cfab49 Release 3.26.1
  • 734c9f2 🐛 fix(asyncio): add exit to BaseAsyncFileLock and fix del loop handli...
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=filelock&package-manager=pip&previous-version=3.25.0&new-version=3.29.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 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 3f591426905..4c8b5c56687 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -71,7 +71,7 @@ exceptiongroup==1.3.1 # via pytest execnet==2.1.2 # via pytest-xdist -filelock==3.25.0 +filelock==3.29.0 # via # python-discovery # virtualenv diff --git a/requirements/dev.txt b/requirements/dev.txt index 39b38894931..03ff327d70f 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -69,7 +69,7 @@ exceptiongroup==1.3.1 # via pytest execnet==2.1.2 # via pytest-xdist -filelock==3.25.0 +filelock==3.29.0 # via # python-discovery # virtualenv diff --git a/requirements/lint.txt b/requirements/lint.txt index 56b407e0345..134975dd424 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -29,7 +29,7 @@ distlib==0.4.0 # via virtualenv exceptiongroup==1.3.1 # via pytest -filelock==3.25.0 +filelock==3.29.0 # via # python-discovery # virtualenv From 5e87b254d6a62192ab9dfe91fd53c22f5e5c6c2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 11:34:02 +0000 Subject: [PATCH 054/191] Bump coverage from 7.13.4 to 7.13.5 (#12492) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.13.4 to 7.13.5.
Changelog

Sourced from coverage's changelog.

Version 7.13.5 — 2026-03-17

  • Fix: issue 2138_ describes a memory leak that happened when repeatedly using the Coverage API with in-memory data. This is now fixed.

  • Fix: the markdown-formatted coverage report didn't fully escape special characters in file paths (issue 2141). This would be very unlikely to cause a problem, but now it's done properly, thanks to Ellie Ayla <pull 2142_>.

  • Fix: the C extension wouldn't build on VS2019, but now it does (issue 2145_).

.. _issue 2138: coveragepy/coveragepy#2138 .. _issue 2141: coveragepy/coveragepy#2141 .. _pull 2142: coveragepy/coveragepy#2142 .. _issue 2145: coveragepy/coveragepy#2145

.. _changes_7-13-4:

Commits
  • c88da14 docs: sample HTML for 7.13.5
  • e2ac3e1 build: sample HTML shouldn't include the status.json file
  • 910f8f3 docs: prep for 7.13.5
  • 3a4819c style: make workflows more uniform
  • 2a53705 chore: bump the action-dependencies group across 1 directory with 4 updates (...
  • e7c878d chore: make upgrade
  • ab4db40 build: use --generate-hashes when pinning
  • a438753 chore: make upgrade
  • 7b33457 refactor: some leftover pyupgrade 3.10 bits
  • 2ff968d refactor: this type wasn't used anywhere
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=coverage&package-manager=pip&previous-version=7.13.4&new-version=7.13.5)](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.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 4c8b5c56687..14b6bfa0d14 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -55,7 +55,7 @@ click==8.3.1 # slotscheck # towncrier # wait-for-it -coverage==7.13.4 +coverage==7.13.5 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/dev.txt b/requirements/dev.txt index 03ff327d70f..c5f7ead1067 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -55,7 +55,7 @@ click==8.3.1 # slotscheck # towncrier # wait-for-it -coverage==7.13.4 +coverage==7.13.5 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test-common.txt b/requirements/test-common.txt index e2519fd4a6b..68722097036 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -14,7 +14,7 @@ cffi==2.0.0 # pytest-codspeed click==8.3.1 # via wait-for-it -coverage==7.13.4 +coverage==7.13.5 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index b6299d50600..07bd4d8b3ac 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -29,7 +29,7 @@ cffi==2.0.0 # pytest-codspeed click==8.3.1 # via wait-for-it -coverage==7.13.4 +coverage==7.13.5 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test.txt b/requirements/test.txt index 623135eb77a..fa75fe59740 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -29,7 +29,7 @@ cffi==2.0.0 # pytest-codspeed click==8.3.1 # via wait-for-it -coverage==7.13.4 +coverage==7.13.5 # via # -r requirements/test-common.in # pytest-cov From eb6f6c12af9fb377f35bc091e86fd27fdfd15301 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 9 May 2026 00:31:00 +0000 Subject: [PATCH 055/191] Bump python-on-whales from 0.80.0 to 0.81.0 (#12472) 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.80.0 to 0.81.0.
Release notes

Sourced from python-on-whales's releases.

v0.81.0

What's Changed

New Contributors

Full Changelog: https://github.com/gabrieldemarmiesse/python-on-whales/compare/v0.80.0...v0.81.0

Commits
  • 059d89a Bump version to 0.81.0
  • f0b529e feat(compose): Add support for docker compose up SERVICE --no-deps (#708)
  • b615fc4 More accurate error handling when streaming stdout and stderr (#703)
  • See full diff in compare view

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 14b6bfa0d14..f308820d5eb 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -199,7 +199,7 @@ python-dateutil==2.9.0.post0 # via freezegun python-discovery==1.3.0 # via virtualenv -python-on-whales==0.80.0 +python-on-whales==0.81.0 # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/dev.txt b/requirements/dev.txt index c5f7ead1067..c77fc891820 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -194,7 +194,7 @@ python-dateutil==2.9.0.post0 # via freezegun python-discovery==1.3.0 # via virtualenv -python-on-whales==0.80.0 +python-on-whales==0.81.0 # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/lint.txt b/requirements/lint.txt index 134975dd424..5a04c5de2b0 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -94,7 +94,7 @@ python-dateutil==2.9.0.post0 # via freezegun python-discovery==1.3.0 # via virtualenv -python-on-whales==0.80.0 +python-on-whales==0.81.0 # via -r requirements/lint.in pyyaml==6.0.3 # via pre-commit diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 68722097036..45b7f565a13 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -83,7 +83,7 @@ pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 # via freezegun -python-on-whales==0.80.0 +python-on-whales==0.81.0 # via -r requirements/test-common.in re-assert==1.1.0 # via -r requirements/test-common.in diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 07bd4d8b3ac..4b07cb3e0be 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -118,7 +118,7 @@ pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 # via freezegun -python-on-whales==0.80.0 +python-on-whales==0.81.0 # via -r requirements/test-common.in re-assert==1.1.0 # via -r requirements/test-common.in diff --git a/requirements/test.txt b/requirements/test.txt index fa75fe59740..83d348e32de 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -118,7 +118,7 @@ pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 # via freezegun -python-on-whales==0.80.0 +python-on-whales==0.81.0 # via -r requirements/test-common.in re-assert==1.1.0 # via -r requirements/test-common.in From e2d40c710e90d9d108a2671bab90097e35d32f2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 9 May 2026 00:36:07 +0000 Subject: [PATCH 056/191] Bump platformdirs from 4.9.2 to 4.9.6 (#12476) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [platformdirs](https://github.com/tox-dev/platformdirs) from 4.9.2 to 4.9.6.
Release notes

Sourced from platformdirs's releases.

4.9.6

What's Changed

Full Changelog: https://github.com/tox-dev/platformdirs/compare/4.9.5...4.9.6

4.9.4

What's Changed

Full Changelog: https://github.com/tox-dev/platformdirs/compare/4.9.3...4.9.4

4.9.3

What's Changed

New Contributors

Full Changelog: https://github.com/tox-dev/platformdirs/compare/4.9.2...4.9.3

Changelog

Sourced from platformdirs's changelog.

########### Changelog ###########


4.9.6 (2026-04-09)


  • 🐛 fix(release): use double quotes for tag variable expansion :pr:477

4.9.5 (2026-04-06)


  • 📝 docs(appauthor): clarify None vs False on Windows :pr:476
  • Separates implementations of macOS dirs that share a default :pr:473 - by :user:Goddesen
  • Remove persist-credentials: false from release job :pr:472
  • fix: do not duplicate site dirs in Unix.iter_{config,site}_dirs() when use_site_for_root is active :pr:469 - by :user:viccie30
  • 🔧 fix(type): resolve ty 0.0.25 type errors :pr:468
  • 🔒 ci(workflows): add zizmor security auditing :pr:467
  • 🐛 fix(release): generate docstrfmt-compatible changelog entries :pr:463

4.9.4 (2026-03-05)


  • [pre-commit.ci] pre-commit autoupdate :pr:461 - by :user:pre-commit-ci[bot]
  • Update README.md
  • 📝 docs: add project logo to documentation :pr:459
  • Standardize .github files to .yaml suffix
  • build(deps): bump the all group with 2 updates :pr:457 - by :user:dependabot[bot]
  • Move SECURITY.md to .github/SECURITY.md
  • Add permissions to workflows :pr:455
  • Add security policy
  • [pre-commit.ci] pre-commit autoupdate :pr:454 - by :user:pre-commit-ci[bot]

4.9.2 (2026-02-16)


  • 📝 docs: restructure following Diataxis framework :pr:448
  • 📝 docs(platforms): fix RST formatting and TOC hierarchy :pr:447

4.9.1 (2026-02-14)


  • 📝 docs: enhance README, fix issues, and reorganize platforms.rst :pr:445

... (truncated)

Commits
  • 56efd77 Release 4.9.6
  • d5d812a 🐛 fix(release): use double quotes for tag variable expansion (#477)
  • c2b0cee build(deps): bump pypa/gh-action-pypi-publish from 1.13.0 to 1.14.0 in the al...
  • 7688069 Release 4.9.5
  • 104d28b 📝 docs(appauthor): clarify None vs False on Windows (#476)
  • 0955048 [pre-commit.ci] pre-commit autoupdate (#475)
  • bd3c766 build(deps): bump astral-sh/setup-uv from 7.6.0 to 8.0.0 in the all group (#474)
  • 749ac3f Separates implementations of macOS dirs that share a default (#473)
  • cb88156 Remove persist-credentials: false from release job (#472)
  • a501eab [pre-commit.ci] pre-commit autoupdate (#470)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=platformdirs&package-manager=pip&previous-version=4.9.2&new-version=4.9.6)](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 f308820d5eb..de8e3a2f0d8 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -140,7 +140,7 @@ pip-tools==7.5.3 # via -r requirements/dev.in pkgconfig==1.5.5 # via -r requirements/test-common.in -platformdirs==4.9.2 +platformdirs==4.9.6 # via # python-discovery # virtualenv diff --git a/requirements/dev.txt b/requirements/dev.txt index c77fc891820..5e8d8d96fdb 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -137,7 +137,7 @@ pip-tools==7.5.3 # via -r requirements/dev.in pkgconfig==1.5.5 # via -r requirements/test-common.in -platformdirs==4.9.2 +platformdirs==4.9.6 # via # python-discovery # virtualenv diff --git a/requirements/lint.txt b/requirements/lint.txt index 5a04c5de2b0..2ab08c76235 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -61,7 +61,7 @@ packaging==26.2 # via pytest pathspec==1.1.1 # via mypy -platformdirs==4.9.2 +platformdirs==4.9.6 # via # python-discovery # virtualenv From e635dd06899ba7a17601e99afa43183b9b514710 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 9 May 2026 02:21:22 +0000 Subject: [PATCH 057/191] Bump mypy from 1.20.2 to 2.0.0 (#12478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mypy](https://github.com/python/mypy) from 1.20.2 to 2.0.0.
Changelog

Sourced from mypy's changelog.

Mypy Release Notes

Next Release

Mypy 2.0

We’ve just uploaded mypy 2.0.0 to the Python Package Index (PyPI). Mypy is a static type checker for Python. This release includes new features, performance improvements and bug fixes. There are also changes to options and defaults. You can install it as follows:

python3 -m pip install -U mypy

You can read the full documentation for this release on Read the Docs.

Enable --local-partial-types by Default

This flag affects the inference of types based on assignments in other scopes. For now, explicitly disabling this continues to be supported, but this support will be removed in the future as the legacy behaviour is hard to support with other current and future features in mypy, like the daemon or the new implementation of flexible redefinitions.

Contributed by Ivan Levkivskyi, Jukka Lehtosalo, Shantanu in PR 21163.

Enable --strict-bytes by Default

Per PEP 688, mypy no longer treats bytearray and memoryview values as assignable to the bytes type.

Contributed by Shantanu in PR 18371.

New Behavior for --allow-redefinition

The --allow-redefinition flag now behaves like --allow-redefinition-new in mypy 1.20 and earlier. The new behavior is generally more flexible. For example, you can have different types for a variable in different blocks:

# mypy: allow-redefinition

def foo(cond: bool) -> None: if cond: for x in ["a", "b"]: # Type of "x" is "str" here ... else: for x in [1, 2]: # Type of "x" is "int" here ...

... (truncated)

Commits

--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sam Bull Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sam Bull --- aiohttp/_websocket/writer.py | 6 +++--- aiohttp/compression_utils.py | 10 +++++----- aiohttp/http_writer.py | 30 +++++++++++++---------------- aiohttp/multipart.py | 37 +++++++++++++++++++++--------------- aiohttp/web_request.py | 7 ++++--- aiohttp/web_response.py | 4 ++-- aiohttp/web_ws.py | 9 ++++++++- 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 +++- 13 files changed, 75 insertions(+), 52 deletions(-) diff --git a/aiohttp/_websocket/writer.py b/aiohttp/_websocket/writer.py index d293171e38b..7daf4fc3394 100644 --- a/aiohttp/_websocket/writer.py +++ b/aiohttp/_websocket/writer.py @@ -158,9 +158,9 @@ def _write_websocket_frame(self, message: bytes, opcode: int, rsv: int) -> None: # when aiohttp is acting as a client. Servers do not use a mask. if use_mask: mask = PACK_RANDBITS(self.get_random_bits()) - message = bytearray(message) - websocket_mask(mask, message) - self.transport.write(header + mask + message) + message_arr = bytearray(message) + websocket_mask(mask, message_arr) + self.transport.write(header + mask + message_arr) self._output_size += MASK_LEN elif msg_length > MSG_SIZE: self.transport.write(header) diff --git a/aiohttp/compression_utils.py b/aiohttp/compression_utils.py index a30894eb0b7..75b24d1cbbf 100644 --- a/aiohttp/compression_utils.py +++ b/aiohttp/compression_utils.py @@ -164,12 +164,12 @@ def __init__( @abstractmethod def decompress_sync( - self, data: bytes, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED + self, data: Buffer, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED ) -> bytes: """Decompress the given data.""" async def decompress( - self, data: bytes, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED + self, data: Buffer, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED ) -> bytes: """Decompress the given data.""" if ( @@ -215,10 +215,10 @@ def __init__( kwargs["level"] = level self._compressor = self._zlib_backend.compressobj(**kwargs) - def compress_sync(self, data: bytes) -> bytes: + def compress_sync(self, data: Buffer) -> bytes: return self._compressor.compress(data) - async def compress(self, data: bytes) -> bytes: + async def compress(self, data: Buffer) -> bytes: """Compress the data and returned the compressed bytes. Note that flush() must be called after the last call to compress() @@ -366,7 +366,7 @@ def __init__( super().__init__(executor=executor, max_sync_chunk_size=max_sync_chunk_size) def decompress_sync( - self, data: bytes, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED + self, data: Buffer, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED ) -> bytes: # zstd uses -1 for unlimited, while zlib uses 0 for unlimited # Convert the zlib convention (0=unlimited) to zstd convention (-1=unlimited) diff --git a/aiohttp/http_writer.py b/aiohttp/http_writer.py index 574bc9c263f..411a2aae882 100644 --- a/aiohttp/http_writer.py +++ b/aiohttp/http_writer.py @@ -11,7 +11,6 @@ List, NamedTuple, Optional, - Union, ) from multidict import CIMultiDict @@ -24,6 +23,13 @@ __all__ = ("StreamWriter", "HttpVersion", "HttpVersion10", "HttpVersion11") +if sys.version_info >= (3, 12): + from collections.abc import Buffer +else: + from typing import Union + + Buffer = Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"] + MIN_PAYLOAD_FOR_WRITELINES = 2048 IS_PY313_BEFORE_313_2 = (3, 13, 0) <= sys.version_info < (3, 13, 2) @@ -45,7 +51,7 @@ class HttpVersion(NamedTuple): HttpVersion11 = HttpVersion(1, 1) -_T_OnChunkSent = Optional[Callable[[bytes], Awaitable[None]]] +_T_OnChunkSent = Optional[Callable[[Buffer], Awaitable[None]]] _T_OnHeadersSent = Optional[Callable[["CIMultiDict[str]"], Awaitable[None]]] @@ -86,7 +92,7 @@ def enable_compression( ) -> None: self._compress = ZLibCompressor(encoding=encoding, strategy=strategy) - def _write(self, chunk: bytes | bytearray | memoryview) -> None: + def _write(self, chunk: Buffer) -> None: size = len(chunk) self.buffer_size += size self.output_size += size @@ -95,7 +101,7 @@ def _write(self, chunk: bytes | bytearray | memoryview) -> None: raise ClientConnectionResetError("Cannot write to closing transport") transport.write(chunk) - def _writelines(self, chunks: Iterable[bytes]) -> None: + def _writelines(self, chunks: Iterable[Buffer]) -> None: size = 0 for chunk in chunks: size += len(chunk) @@ -109,18 +115,12 @@ def _writelines(self, chunks: Iterable[bytes]) -> None: else: transport.writelines(chunks) - def _write_chunked_payload( - self, chunk: Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"] - ) -> None: + def _write_chunked_payload(self, chunk: Buffer) -> None: """Write a chunk with proper chunked encoding.""" chunk_len_pre = f"{len(chunk):x}\r\n".encode("ascii") self._writelines((chunk_len_pre, chunk, b"\r\n")) - def _send_headers_with_payload( - self, - chunk: Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"], - is_eof: bool, - ) -> None: + def _send_headers_with_payload(self, chunk: Buffer, is_eof: bool) -> None: """Send buffered headers with payload, coalescing into single write.""" # Mark headers as written self._headers_written = True @@ -153,11 +153,7 @@ def _send_headers_with_payload( self._write(headers_buf) async def write( - self, - chunk: bytes | bytearray | memoryview, - *, - drain: bool = True, - LIMIT: int = 0x10000, + self, chunk: Buffer, *, drain: bool = True, LIMIT: int = 0x10000 ) -> None: """ Writes chunk of data to a stream. diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py index 5cea890d343..7cc3d4db0e8 100644 --- a/aiohttp/multipart.py +++ b/aiohttp/multipart.py @@ -8,7 +8,7 @@ from collections import deque from collections.abc import AsyncIterator, Iterator, Mapping, Sequence from types import TracebackType -from typing import TYPE_CHECKING, Any, Union, cast +from typing import TYPE_CHECKING, Any, TypeVar, Union, cast from urllib.parse import parse_qsl, unquote, urlencode from multidict import CIMultiDict, CIMultiDictProxy @@ -40,10 +40,15 @@ if sys.version_info >= (3, 11): from typing import Self else: - from typing import TypeVar - Self = TypeVar("Self", bound="BodyPartReader") +if sys.version_info >= (3, 12): + from collections.abc import Buffer +else: + Buffer = Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"] + +_Buffer = TypeVar("_Buffer", bound=Buffer) + __all__ = ( "MultipartReader", "MultipartWriter", @@ -499,7 +504,7 @@ def at_eof(self) -> bool: """Returns True if the boundary was reached or False otherwise.""" return self._at_eof - def _apply_content_transfer_decoding(self, data: bytes) -> bytes: + def _apply_content_transfer_decoding(self, data: _Buffer) -> _Buffer | bytes: """Apply Content-Transfer-Encoding decoding if header is present.""" if CONTENT_TRANSFER_ENCODING in self.headers: return self._decode_content_transfer(data) @@ -510,7 +515,7 @@ def _needs_content_decoding(self) -> bool: # https://datatracker.ietf.org/doc/html/rfc7578#section-4.8 return not self._is_form_data and CONTENT_ENCODING in self.headers - def decode(self, data: bytes) -> bytes: + def decode(self, data: _Buffer) -> _Buffer | bytes: """Decodes data synchronously. Decodes data according the specified Content-Encoding @@ -519,12 +524,12 @@ def decode(self, data: bytes) -> bytes: Note: For large payloads, consider using decode_iter() instead to avoid blocking the event loop during decompression. """ - data = self._apply_content_transfer_decoding(data) + decoded = self._apply_content_transfer_decoding(data) if self._needs_content_decoding(): - return self._decode_content(data) - return data + return self._decode_content(decoded) + return decoded - async def decode_iter(self, data: bytes) -> AsyncIterator[bytes]: + async def decode_iter(self, data: _Buffer) -> AsyncIterator[_Buffer | bytes]: """Async generator that yields decoded data chunks. Decodes data according the specified Content-Encoding @@ -533,14 +538,14 @@ async def decode_iter(self, data: bytes) -> AsyncIterator[bytes]: This method offloads decompression to an executor for large payloads to avoid blocking the event loop. """ - data = self._apply_content_transfer_decoding(data) + decoded = self._apply_content_transfer_decoding(data) if self._needs_content_decoding(): - async for d in self._decode_content_async(data): + async for d in self._decode_content_async(decoded): yield d else: - yield data + yield decoded - def _decode_content(self, data: bytes) -> bytes: + def _decode_content(self, data: _Buffer) -> _Buffer | bytes: encoding = self.headers.get(CONTENT_ENCODING, "").lower() if encoding == "identity": return data @@ -552,7 +557,9 @@ def _decode_content(self, data: bytes) -> bytes: raise RuntimeError(f"unknown content encoding: {encoding}") - async def _decode_content_async(self, data: bytes) -> AsyncIterator[bytes]: + async def _decode_content_async( + self, data: _Buffer + ) -> AsyncIterator[_Buffer | bytes]: encoding = self.headers.get(CONTENT_ENCODING, "").lower() if encoding == "identity": yield data @@ -567,7 +574,7 @@ async def _decode_content_async(self, data: bytes) -> AsyncIterator[bytes]: else: raise RuntimeError(f"unknown content encoding: {encoding}") - def _decode_content_transfer(self, data: bytes) -> bytes: + def _decode_content_transfer(self, data: _Buffer) -> _Buffer | bytes: encoding = self.headers.get(CONTENT_TRANSFER_ENCODING, "").lower() if encoding == "base64": diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py index 25d7ebc8e22..b5d5b1b6897 100644 --- a/aiohttp/web_request.py +++ b/aiohttp/web_request.py @@ -76,6 +76,7 @@ class FileField: headers: CIMultiDictProxy[str] +_Post = str | bytes | bytearray | FileField _TCHAR: Final[str] = string.digits + string.ascii_letters + r"!#$%&'*+.^_`|~-" # '-' at the end to prevent interpretation as range in a char class @@ -134,7 +135,7 @@ class BaseRequest(MutableMapping[str | RequestKey[Any], Any], HeadersMixin): "_transport_peername", ] ) - _post: MultiDictProxy[str | bytes | FileField] | None = None + _post: MultiDictProxy[_Post] | None = None _read_bytes: bytes | None = None _seen_str_keys: set[str] = set() @@ -717,7 +718,7 @@ async def multipart(self) -> MultipartReader: max_size_error_cls=HTTPRequestEntityTooLarge, ) - async def post(self) -> "MultiDictProxy[str | bytes | FileField]": + async def post(self) -> "MultiDictProxy[_Post]": """Return POST parameters.""" if self._post is not None: return self._post @@ -734,7 +735,7 @@ async def post(self) -> "MultiDictProxy[str | bytes | FileField]": self._post = MultiDictProxy(MultiDict()) return self._post - out: MultiDict[str | bytes | FileField] = MultiDict() + out: MultiDict[_Post] = MultiDict() if content_type == "multipart/form-data": multipart = await self.multipart() diff --git a/aiohttp/web_response.py b/aiohttp/web_response.py index 7a14ada4dbb..daf29eccce4 100644 --- a/aiohttp/web_response.py +++ b/aiohttp/web_response.py @@ -720,7 +720,7 @@ def __init__( self._zlib_executor = zlib_executor @property - def body(self) -> bytes | Payload | None: + def body(self) -> bytes | bytearray | Payload | None: return self._body @body.setter @@ -799,7 +799,7 @@ async def write_eof(self, data: bytes = b"") -> None: if self._eof_sent: return if self._compressed_body is None: - body: bytes | Payload | None = self._body + body = self._body else: body = self._compressed_body assert not data, f"data arg is not supported, got {data!r}" diff --git a/aiohttp/web_ws.py b/aiohttp/web_ws.py index 5aa75b715a0..c01d95637cd 100644 --- a/aiohttp/web_ws.py +++ b/aiohttp/web_ws.py @@ -48,6 +48,13 @@ else: from typing_extensions import TypeVar +if sys.version_info >= (3, 12): + from collections.abc import Buffer +else: + from typing import Union + + Buffer = Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"] + if sys.version_info >= (3, 11): import asyncio as async_timeout from typing import Self @@ -740,7 +747,7 @@ async def receive_json( data = await self.receive_str(timeout=timeout) return loads(data) # type: ignore[arg-type] - async def write(self, data: bytes) -> None: + async def write(self, data: Buffer) -> None: raise RuntimeError("Cannot call .write() for websocket") def __aiter__(self) -> Self: diff --git a/requirements/constraints.txt b/requirements/constraints.txt index de8e3a2f0d8..19c261bf849 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -18,6 +18,8 @@ alabaster==1.0.0 # via sphinx annotated-types==0.7.0 # via pydantic +ast-serialize==0.3.0 + # via mypy async-timeout==5.0.1 ; python_version < "3.11" # via # -r requirements/runtime-deps.in @@ -119,7 +121,7 @@ multidict==6.7.1 # -r requirements/multidict.in # -r requirements/runtime-deps.in # yarl -mypy==1.20.2 ; implementation_name == "cpython" +mypy==2.0.0 ; implementation_name == "cpython" # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 5e8d8d96fdb..3b93809d388 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -18,6 +18,8 @@ alabaster==1.0.0 # via sphinx annotated-types==0.7.0 # via pydantic +ast-serialize==0.3.0 + # via mypy async-timeout==5.0.1 ; python_version < "3.11" # via # -r requirements/runtime-deps.in @@ -116,7 +118,7 @@ multidict==6.7.1 # via # -r requirements/runtime-deps.in # yarl -mypy==1.20.2 ; implementation_name == "cpython" +mypy==2.0.0 ; implementation_name == "cpython" # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/lint.txt b/requirements/lint.txt index 2ab08c76235..e34c078bb31 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -8,6 +8,8 @@ aiodns==3.6.1 # via -r requirements/lint.in annotated-types==0.7.0 # via pydantic +ast-serialize==0.3.0 + # via mypy async-timeout==5.0.1 # via valkey backports-zstd==1.3.0 ; implementation_name == "cpython" and python_version < "3.14" @@ -51,7 +53,7 @@ markdown-it-py==4.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -mypy==1.20.2 ; implementation_name == "cpython" +mypy==2.0.0 ; implementation_name == "cpython" # via -r requirements/lint.in mypy-extensions==1.1.0 # via mypy diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 45b7f565a13..706ee9d76a6 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -6,6 +6,8 @@ # annotated-types==0.7.0 # via pydantic +ast-serialize==0.3.0 + # via mypy blockbuster==1.5.26 # via -r requirements/test-common.in cffi==2.0.0 @@ -40,7 +42,7 @@ markdown-it-py==4.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -mypy==1.20.2 ; implementation_name == "cpython" +mypy==2.0.0 ; implementation_name == "cpython" # via -r requirements/test-common.in mypy-extensions==1.1.0 # via mypy diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 4b07cb3e0be..6b886291c03 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -12,6 +12,8 @@ aiosignal==1.4.0 # via -r requirements/runtime-deps.in annotated-types==0.7.0 # via pydantic +ast-serialize==0.3.0 + # via mypy async-timeout==5.0.1 ; python_version < "3.11" # via -r requirements/runtime-deps.in attrs==26.1.0 @@ -67,7 +69,7 @@ multidict==6.7.1 # via # -r requirements/runtime-deps.in # yarl -mypy==1.20.2 ; implementation_name == "cpython" +mypy==2.0.0 ; implementation_name == "cpython" # via -r requirements/test-common.in mypy-extensions==1.1.0 # via mypy diff --git a/requirements/test.txt b/requirements/test.txt index 83d348e32de..50c05e00ff6 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -12,6 +12,8 @@ aiosignal==1.4.0 # via -r requirements/runtime-deps.in annotated-types==0.7.0 # via pydantic +ast-serialize==0.3.0 + # via mypy async-timeout==5.0.1 ; python_version < "3.11" # via -r requirements/runtime-deps.in attrs==26.1.0 @@ -67,7 +69,7 @@ multidict==6.7.1 # via # -r requirements/runtime-deps.in # yarl -mypy==1.20.2 ; implementation_name == "cpython" +mypy==2.0.0 ; implementation_name == "cpython" # via -r requirements/test-common.in mypy-extensions==1.1.0 # via mypy From 9b89e732981e4949f8c4bbdc306f8e4efe76955e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 9 May 2026 02:39:43 +0000 Subject: [PATCH 058/191] Bump rich from 14.3.3 to 15.0.0 (#12480) Bumps [rich](https://github.com/Textualize/rich) from 14.3.3 to 15.0.0.
Release notes

Sourced from rich's releases.

The So Long 3.8 Release

A few fixes. The major version bump is to honor the passing of 3.8 support which reached its EOL in October 7, 2024

[15.0.0] - 2026-04-12

Changed

  • Breaking change: Dropped support for Python3.8

Fixed

The Faster Startup Release

No new features in this release, but there should be improved startup time for Rich apps, and potentially improved runtime if you have a lot of links.

[14.3.4] - 2026-04-11

Changed

Changelog

Sourced from rich's changelog.

[15.0.0] - 2026-04-12

Changed

  • Breaking change: Dropped support for Python3.8

Fixed

[14.3.4] - 2026-04-11

Changed

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 19c261bf849..4c995ceb3a6 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -215,7 +215,7 @@ requests==2.33.1 # via # sphinx # sphinxcontrib-spelling -rich==14.3.3 +rich==15.0.0 # via pytest-codspeed setuptools-git==1.2 # via -r requirements/test-common.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 3b93809d388..feede4999b0 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -208,7 +208,7 @@ regex==2026.2.28 # via re-assert requests==2.33.1 # via sphinx -rich==14.3.3 +rich==15.0.0 # via pytest-codspeed setuptools-git==1.2 # via -r requirements/test-common.in diff --git a/requirements/lint.txt b/requirements/lint.txt index e34c078bb31..fab751254b4 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -100,7 +100,7 @@ python-on-whales==0.81.0 # via -r requirements/lint.in pyyaml==6.0.3 # via pre-commit -rich==14.3.3 +rich==15.0.0 # via pytest-codspeed six==1.17.0 # via python-dateutil diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 706ee9d76a6..a2036fca622 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -91,7 +91,7 @@ re-assert==1.1.0 # via -r requirements/test-common.in regex==2026.2.28 # via re-assert -rich==14.3.3 +rich==15.0.0 # via pytest-codspeed setuptools-git==1.2 # via -r requirements/test-common.in diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 6b886291c03..ecc5acde368 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -126,7 +126,7 @@ re-assert==1.1.0 # via -r requirements/test-common.in regex==2026.2.28 # via re-assert -rich==14.3.3 +rich==15.0.0 # via pytest-codspeed setuptools-git==1.2 # via -r requirements/test-common.in diff --git a/requirements/test.txt b/requirements/test.txt index 50c05e00ff6..cbfde72c1e5 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -126,7 +126,7 @@ re-assert==1.1.0 # via -r requirements/test-common.in regex==2026.2.28 # via re-assert -rich==14.3.3 +rich==15.0.0 # via pytest-codspeed setuptools-git==1.2 # via -r requirements/test-common.in From 9e1d37fc55f41e221e66f5913098f4694f798ebc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 10 May 2026 23:02:50 +0000 Subject: [PATCH 059/191] Bump cryptography from 46.0.5 to 48.0.0 (#12491) Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.5 to 48.0.0.
Changelog

Sourced from cryptography's changelog.

48.0.0 - 2026-05-04


* **BACKWARDS INCOMPATIBLE:** Support for Python 3.8 has been removed.
  ``cryptography`` now requires Python 3.9 or later.
* **BACKWARDS INCOMPATIBLE:** Loading an X.509 CRL whose inner
  ``TBSCertList.signature`` algorithm does not match the outer
``signatureAlgorithm`` now raises ``ValueError``. Previously, such CRLs
were parsed successfully and only rejected during signature validation.
* Added support for :doc:`/hazmat/primitives/asymmetric/mlkem` and
  :doc:`/hazmat/primitives/asymmetric/mldsa` when using OpenSSL 3.5.0 or
later, in addition to the existing AWS-LC and BoringSSL support. This
means
  post-quantum algorithms are now available to users of our wheels.
  • Note: Going forward, we do not guarantee that all functionality
    in cryptography will be available when building against
    OpenSSL. See :doc:/statements/state-of-openssl for more information.

.. _v47-0-0:

47.0.0 - 2026-04-24

  • Support for Python 3.8 is deprecated and will be removed in the next cryptography release.
  • BACKWARDS INCOMPATIBLE: Support for binary elliptic curves (SECT* classes) has been removed. These curves are rarely used and have additional security considerations that make them undesirable.
  • BACKWARDS INCOMPATIBLE: Support for OpenSSL 1.1.x has been removed. OpenSSL 3.0.0 or later is now required. LibreSSL, BoringSSL, and AWS-LC continue to be supported.
  • BACKWARDS INCOMPATIBLE: Dropped support for LibreSSL < 4.1.
  • BACKWARDS INCOMPATIBLE: Loading keys with unsupported algorithms or keys with unsupported explicit curve encodings now raises :class:~cryptography.exceptions.UnsupportedAlgorithm instead of ValueError. This change affects :func:~cryptography.hazmat.primitives.serialization.load_pem_private_key, :func:~cryptography.hazmat.primitives.serialization.load_der_private_key, :func:~cryptography.hazmat.primitives.serialization.load_pem_public_key, :func:~cryptography.hazmat.primitives.serialization.load_der_public_key, and :meth:~cryptography.x509.Certificate.public_key when called on certificates with unsupported public key algorithms.
  • BACKWARDS INCOMPATIBLE: When parsing elliptic curve private keys, we now reject keys that incorrectly encode a private key of the wrong length because such keys are impossible to process in a constant-time manner. We do not believe keys with this problem are in wide use, however we may revert this change based on the feedback we receive.
  • Deprecated passing 64-bit (8-byte) and 128-bit (16-byte) keys to :class:~cryptography.hazmat.decrepit.ciphers.algorithms.TripleDES. In a

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cryptography&package-manager=pip&previous-version=46.0.5&new-version=48.0.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 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-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 4c995ceb3a6..90d2b53815c 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -61,7 +61,7 @@ coverage==7.13.5 # via # -r requirements/test-common.in # pytest-cov -cryptography==46.0.5 +cryptography==48.0.0 # via trustme cython==3.2.4 # via -r requirements/cython.in diff --git a/requirements/dev.txt b/requirements/dev.txt index feede4999b0..259c997c64a 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -61,7 +61,7 @@ coverage==7.13.5 # via # -r requirements/test-common.in # pytest-cov -cryptography==46.0.5 +cryptography==48.0.0 # via trustme distlib==0.4.0 # via virtualenv diff --git a/requirements/lint.txt b/requirements/lint.txt index fab751254b4..36a222767e0 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -25,7 +25,7 @@ cfgv==3.5.0 # via pre-commit click==8.3.1 # via slotscheck -cryptography==46.0.5 +cryptography==48.0.0 # via trustme distlib==0.4.0 # via virtualenv diff --git a/requirements/test-common.txt b/requirements/test-common.txt index a2036fca622..4f76e4f26f4 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -20,7 +20,7 @@ coverage==7.13.5 # via # -r requirements/test-common.in # pytest-cov -cryptography==46.0.5 +cryptography==48.0.0 # via trustme exceptiongroup==1.3.1 # via pytest diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index ecc5acde368..a2dbdb2db46 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -35,7 +35,7 @@ coverage==7.13.5 # via # -r requirements/test-common.in # pytest-cov -cryptography==46.0.5 +cryptography==48.0.0 # via trustme exceptiongroup==1.3.1 # via pytest diff --git a/requirements/test.txt b/requirements/test.txt index cbfde72c1e5..a4aa15f4698 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -35,7 +35,7 @@ coverage==7.13.5 # via # -r requirements/test-common.in # pytest-cov -cryptography==46.0.5 +cryptography==48.0.0 # via trustme exceptiongroup==1.3.1 # via pytest From 3f7231c32570f1bb11163b57c156dba0deb8edd8 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 11 May 2026 00:24:07 +0100 Subject: [PATCH 060/191] Deprecate aiohttp.pytest_plugin (#10785) --- CHANGES/10785.misc.rst | 1 + aiohttp/pytest_plugin.py | 4 ++++ setup.cfg | 2 ++ 3 files changed, 7 insertions(+) create mode 100644 CHANGES/10785.misc.rst diff --git a/CHANGES/10785.misc.rst b/CHANGES/10785.misc.rst new file mode 100644 index 00000000000..5b43f63a42d --- /dev/null +++ b/CHANGES/10785.misc.rst @@ -0,0 +1 @@ +Added deprecation warning to ``aiohttp.pytest_plugin``, please switch to ``pytest-aiohttp`` -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/pytest_plugin.py b/aiohttp/pytest_plugin.py index 7ea6be2f335..a2c7bdca03e 100644 --- a/aiohttp/pytest_plugin.py +++ b/aiohttp/pytest_plugin.py @@ -198,6 +198,10 @@ def pytest_pyfunc_call(pyfuncitem): # type: ignore[no-untyped-def] """Run coroutines in an event loop instead of a normal function call.""" fast = pyfuncitem.config.getoption("--aiohttp-fast") if inspect.iscoroutinefunction(pyfuncitem.function): + warnings.warn( + "aiohttp.pytest_plugin will be removed in v4. Please install pytest-aiohttp.", + DeprecationWarning, + ) existing_loop = ( pyfuncitem.funcargs.get("proactor_loop") or pyfuncitem.funcargs.get("selector_loop") diff --git a/setup.cfg b/setup.cfg index 62f68b11eab..5df4444e507 100644 --- a/setup.cfg +++ b/setup.cfg @@ -80,6 +80,8 @@ filterwarnings = ignore:datetime.*utcnow\(\) is deprecated and scheduled for removal:DeprecationWarning:freezegun.api # Weird issue in Python 3.13+ triggered in test_multipart.py ignore:coroutine method 'aclose' of 'BodyPartReader._decode_content_async' was never awaited:RuntimeWarning + # Our own warning + ignore:aiohttp.pytest_plugin will be removed in v4:DeprecationWarning junit_suite_name = aiohttp_test_suite norecursedirs = dist docs build .tox .eggs minversion = 3.8.2 From 50beb8de9b63739d1f0a2fb69a140526b1bbd42b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 00:04:35 +0000 Subject: [PATCH 061/191] Bump click from 8.3.1 to 8.3.3 (#12489) Bumps [click](https://github.com/pallets/click) from 8.3.1 to 8.3.3.
Release notes

Sourced from click's releases.

8.3.3

This is the Click 8.3.3 fix release, which fixes bugs but does not otherwise change behavior and should not result in breaking changes compared to the latest feature release.

PyPI: https://pypi.org/project/click/8.3.3/ Changes: https://click.palletsprojects.com/page/changes/#version-8-3-3 Milestone: https://github.com/pallets/click/milestone/30

  • Use :func:shlex.split to split pager and editor commands into argv lists for :class:subprocess.Popen, removing shell=True. #1026 #1477 #2775
  • Fix TypeError when rendering help for an option whose default value is an object that doesn't support equality comparison with strings, such as semver.Version. #3298 #3299
  • Fix pager test pollution under parallel execution by using pytest's tmp_path fixture instead of a shared temporary file path. #3238
  • Treat Sentinel.UNSET values in a default_map as absent, so they fall through to the next default source instead of being used as the value. #3224 #3240
  • Patch pdb.Pdb in CliRunner isolation so pdb.set_trace(), breakpoint(), and debuggers subclassing pdb.Pdb (ipdb, pdbpp) can interact with the real terminal instead of the captured I/O streams. #654 #824 #843 #951 #3235
  • Add optional randomized parallel test execution using pytest-randomly and pytest-xdist to detect test pollution and race conditions. #3151
  • Add contributor documentation for running stress tests, randomized parallel tests, and Flask smoke tests. #3151 #3177
  • Show custom show_default string in prompts, matching the existing help text behavior. #2836 #2837 #3165 #3262 #3280 #3328
  • Fix default=True with boolean flag_value always returning the flag_value instead of True. The default=True to flag_value substitution now only applies to non-boolean flags, where True acts as a sentinel meaning "activate this flag by default". For boolean flags, default=True is returned as a literal value. #3111 #3239
  • Mark make_default_short_help as private API. #3189 #3250
  • CliRunner's redirected streams now expose the original file descriptor via fileno(), so that faulthandler, subprocess, and other C-level consumers no longer crash with io.UnsupportedOperation. #2865
  • Change :class:ParameterSource to an :class:~enum.IntEnum and reorder its members from most to least explicit, so values can be compared to check whether a parameter was explicitly provided. #2879 #3248

8.3.2

This is the Click 8.3.2 fix release, which fixes bugs but does not otherwise change behavior and should not result in breaking changes compared to the latest feature release.

PyPI: https://pypi.org/project/click/8.3.2/ Changes: https://click.palletsprojects.com/page/changes/#version-8-3-2 Milestone: https://github.com/pallets/click/milestone/29

... (truncated)

Changelog

Sourced from click's changelog.

Version 8.3.3

Released 2026-04-20

  • Use :func:shlex.split to split pager and editor commands into argv lists for :class:subprocess.Popen, removing shell=True. :issue:1026 :pr:1477 :pr:2775
  • Fix TypeError when rendering help for an option whose default value is an object that doesn't support equality comparison with strings, such as semver.Version. :issue:3298 :pr:3299
  • Fix pager test pollution under parallel execution by using pytest's tmp_path fixture instead of a shared temporary file path. :pr:3238
  • Treat Sentinel.UNSET values in a default_map as absent, so they fall through to the next default source instead of being used as the value. :issue:3224 :pr:3240
  • Patch pdb.Pdb in CliRunner isolation so pdb.set_trace(), breakpoint(), and debuggers subclassing pdb.Pdb (ipdb, pdbpp) can interact with the real terminal instead of the captured I/O streams. :issue:654 :issue:824 :issue:843 :pr:951 :pr:3235
  • Add optional randomized parallel test execution using pytest-randomly and pytest-xdist to detect test pollution and race conditions. :pr:3151
  • Add contributor documentation for running stress tests, randomized parallel tests, and Flask smoke tests. :pr:3151 :pr:3177
  • Show custom show_default string in prompts, matching the existing help text behavior. :issue:2836 :pr:2837 :pr:3165 :pr:3262 :pr:3280 :pr:3328
  • Fix default=True with boolean flag_value always returning the flag_value instead of True. The default=True to flag_value substitution now only applies to non-boolean flags, where True acts as a sentinel meaning "activate this flag by default". For boolean flags, default=True is returned as a literal value. :issue:3111 :pr:3239
  • Mark make_default_short_help as private API. :issue:3189 :pr:3250
  • CliRunner's redirected streams now expose the original file descriptor via fileno(), so that faulthandler, subprocess, and other C-level consumers no longer crash with io.UnsupportedOperation. :issue:2865
  • Change :class:ParameterSource to an :class:~enum.IntEnum and reorder its members from most to least explicit, so values can be compared to check whether a parameter was explicitly provided. :issue:2879 :pr:3248

Version 8.3.2

Released 2026-04-02

  • Fix handling of flag_value when is_flag=False to allow such options to be used without an explicit value. :issue:3084 :pr:3152
  • Hide Sentinel.UNSET values as None when using lookup_default(). :issue:3136 :pr:3199 :pr:3202 :pr:3209 :pr:3212 :pr:3224

... (truncated)

Commits
  • c06d2d0 Release 8.3.3
  • f1f191e Apply format guidelines to commits since latest 8.3.2 release (#3343)
  • bb59ba0 Apply format guidelines to commits since latest 8.3.2 release
  • 4a35225 Reduce blast-radius of UNSET in default_map (#3240)
  • c07bb93 Merge branch 'stable' into unset-in-default-map
  • c7e1ba8 Reorder ParameterSource (#3248)
  • 76552ff Show default string in prompt (#3328)
  • ac5cec5 Reorder ParameterSource from most to least explicit
  • 8c452e0 Merge branch 'stable' into show-default-string-in-prompt
  • 8c95c73 Reconcile default value passing and default activation (#3239)
  • Additional commits viewable in compare view

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 +- requirements/lint.txt | 2 +- requirements/test-common.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 90d2b53815c..e02ab35bae9 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -51,7 +51,7 @@ cfgv==3.5.0 # via pre-commit charset-normalizer==3.4.7 # via requests -click==8.3.1 +click==8.3.3 # via # pip-tools # slotscheck diff --git a/requirements/dev.txt b/requirements/dev.txt index 259c997c64a..de8e14beb83 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -51,7 +51,7 @@ cfgv==3.5.0 # via pre-commit charset-normalizer==3.4.7 # via requests -click==8.3.1 +click==8.3.3 # via # pip-tools # slotscheck diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index 88eea72170c..c486dab30a5 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -14,7 +14,7 @@ certifi==2026.2.25 # via requests charset-normalizer==3.4.7 # via requests -click==8.3.1 +click==8.3.3 # via towncrier docutils==0.21.2 # via sphinx diff --git a/requirements/doc.txt b/requirements/doc.txt index 4cff982765e..bfd975c6495 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -14,7 +14,7 @@ certifi==2026.2.25 # via requests charset-normalizer==3.4.7 # via requests -click==8.3.1 +click==8.3.3 # via towncrier docutils==0.21.2 # via sphinx diff --git a/requirements/lint.txt b/requirements/lint.txt index 36a222767e0..369afd866c5 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -23,7 +23,7 @@ cffi==2.0.0 # pytest-codspeed cfgv==3.5.0 # via pre-commit -click==8.3.1 +click==8.3.3 # via slotscheck cryptography==48.0.0 # via trustme diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 4f76e4f26f4..55648c11515 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -14,7 +14,7 @@ cffi==2.0.0 # via # cryptography # pytest-codspeed -click==8.3.1 +click==8.3.3 # via wait-for-it coverage==7.13.5 # via diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index a2dbdb2db46..b0ec679c219 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -29,7 +29,7 @@ cffi==2.0.0 # cryptography # pycares # pytest-codspeed -click==8.3.1 +click==8.3.3 # via wait-for-it coverage==7.13.5 # via diff --git a/requirements/test.txt b/requirements/test.txt index a4aa15f4698..95054660a7a 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -29,7 +29,7 @@ cffi==2.0.0 # cryptography # pycares # pytest-codspeed -click==8.3.1 +click==8.3.3 # via wait-for-it coverage==7.13.5 # via From 75e08fc9b1082af63d60a8a4fbe717ac0acb5ba6 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 01:05:27 +0100 Subject: [PATCH 062/191] [PR #12493/76d3a7f1 backport][3.14] Don't re-await main_task in run_app finally when it's already done (#12496) **This is a backport of PR #12493 as merged into master (76d3a7f12aa088346fafb1cfd82f9c591010d336).** Co-authored-by: Andrew Karelin <36686667+AndrewKarelin@users.noreply.github.com> --- CHANGES/12493.bugfix | 3 +++ aiohttp/web.py | 15 ++++++++++++--- tests/test_run_app.py | 23 +++++++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 CHANGES/12493.bugfix diff --git a/CHANGES/12493.bugfix b/CHANGES/12493.bugfix new file mode 100644 index 00000000000..7a68daec0ba --- /dev/null +++ b/CHANGES/12493.bugfix @@ -0,0 +1,3 @@ +Fixed :func:`aiohttp.web.run_app` losing inner traceback frames when an +exception is raised during application startup (e.g. inside +``cleanup_ctx`` or ``on_startup``). Regression since 3.10.6. diff --git a/aiohttp/web.py b/aiohttp/web.py index 071f51d9fe2..f717d94bfba 100644 --- a/aiohttp/web.py +++ b/aiohttp/web.py @@ -504,9 +504,18 @@ def run_app( pass finally: try: - main_task.cancel() - with suppress(asyncio.CancelledError): - loop.run_until_complete(main_task) + # Skip when ``main_task`` is already done (e.g. raised during startup). + # Re-running ``loop.run_until_complete`` on a finished task calls + # ``Future.result`` again, which does + # ``raise self._exception.with_traceback(self._exception_tb)`` and + # resets ``exc.__traceback__`` to the originally saved tb — by then + # shallow — clobbering the deep traceback the caller would otherwise + # see (frames from ``cleanup_ctx`` / ``on_startup`` and the user code + # that actually raised). + if not main_task.done(): + main_task.cancel() + with suppress(asyncio.CancelledError): + loop.run_until_complete(main_task) finally: _cancel_tasks(asyncio.all_tasks(loop), loop) loop.run_until_complete(loop.shutdown_asyncgens()) diff --git a/tests/test_run_app.py b/tests/test_run_app.py index 899064aa165..f855fbe0a06 100644 --- a/tests/test_run_app.py +++ b/tests/test_run_app.py @@ -9,6 +9,7 @@ import subprocess import sys import time +import traceback from collections.abc import AsyncIterator, Awaitable, Callable, Coroutine, Iterator from typing import NoReturn from unittest import mock @@ -115,6 +116,28 @@ def test_run_app_close_loop(patched_loop) -> None: assert patched_loop.is_closed() +def test_run_app_preserves_startup_traceback( + patched_loop: asyncio.AbstractEventLoop, +) -> None: + # Regression: when an exception is raised during startup (here in a + # cleanup_ctx async generator), the user code frame must remain in the + # traceback that propagates out of run_app. Previously the second + # loop.run_until_complete(main_task) in run_app's finally clobbered it. + + async def failing_ctx(_app: web.Application) -> AsyncIterator[None]: + raise RuntimeError("boom from failing_ctx") + yield # type: ignore[unreachable] # required to make this an async generator + + app = web.Application() + app.cleanup_ctx.append(failing_ctx) + + with pytest.raises(RuntimeError, match="boom from failing_ctx") as exc_info: + web.run_app(app, print=None, loop=patched_loop) + + frames = [f.name for f in traceback.extract_tb(exc_info.tb)] + assert "failing_ctx" in frames, frames + + mock_unix_server_single = [ mock.call(mock.ANY, "/tmp/testsock1.sock", ssl=None, backlog=128), ] From 6b98c2b5848a53214a84fb5594d9194b756750ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 17:15:17 +0000 Subject: [PATCH 063/191] Bump idna from 3.13 to 3.14 (#12505) Bumps [idna](https://github.com/kjd/idna) from 3.13 to 3.14.
Changelog

Sourced from idna's changelog.

3.14 (2026-05-10) +++++++++++++++++

  • Removed opportunity to process long inputs into quadratic time by rejecting oversize inputs up-front. Closes a bypass of the CVE-2024-3651 mitigation. [GHSA-65pc-fj4g-8rjx]

Thanks to Stan Ulbrych for reporting the issue.

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=idna&package-manager=pip&previous-version=3.13&new-version=3.14)](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/base-ft.txt | 2 +- requirements/base.txt | 2 +- requirements/constraints.txt | 2 +- requirements/dev.txt | 2 +- requirements/doc-spelling.txt | 2 +- requirements/doc.txt | 2 +- requirements/lint.txt | 2 +- requirements/runtime-deps.txt | 2 +- requirements/test-common.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt index 1f6f712fee4..0374faf854b 100644 --- a/requirements/base-ft.txt +++ b/requirements/base-ft.txt @@ -26,7 +26,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==26.0.0 # via -r requirements/base-ft.in -idna==3.13 +idna==3.14 # via yarl multidict==6.7.1 # via diff --git a/requirements/base.txt b/requirements/base.txt index a8a4c998885..bd1e85fa517 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -26,7 +26,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==26.0.0 # via -r requirements/base.in -idna==3.13 +idna==3.14 # via yarl multidict==6.7.1 # via diff --git a/requirements/constraints.txt b/requirements/constraints.txt index e02ab35bae9..41006350232 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -91,7 +91,7 @@ gunicorn==26.0.0 # via -r requirements/base.in identify==2.6.17 # via pre-commit -idna==3.13 +idna==3.14 # via # requests # trustme diff --git a/requirements/dev.txt b/requirements/dev.txt index de8e14beb83..b3403b8d3e0 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -89,7 +89,7 @@ gunicorn==26.0.0 # via -r requirements/base.in identify==2.6.17 # via pre-commit -idna==3.13 +idna==3.14 # via # requests # trustme diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index c486dab30a5..ad7435ac2a9 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -18,7 +18,7 @@ click==8.3.3 # via towncrier docutils==0.21.2 # via sphinx -idna==3.13 +idna==3.14 # via requests imagesize==1.5.0 # via sphinx diff --git a/requirements/doc.txt b/requirements/doc.txt index bfd975c6495..e1843cbbe1d 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -18,7 +18,7 @@ click==8.3.3 # via towncrier docutils==0.21.2 # via sphinx -idna==3.13 +idna==3.14 # via requests imagesize==1.5.0 # via sphinx diff --git a/requirements/lint.txt b/requirements/lint.txt index 369afd866c5..e3c548c117f 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -41,7 +41,7 @@ freezegun==1.5.5 # via -r requirements/lint.in identify==2.6.17 # via pre-commit -idna==3.13 +idna==3.14 # via trustme iniconfig==2.3.0 # via pytest diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index 9eb626271a1..54928256f50 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -24,7 +24,7 @@ frozenlist==1.8.0 # via # -r requirements/runtime-deps.in # aiosignal -idna==3.13 +idna==3.14 # via yarl multidict==6.7.1 # via diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 55648c11515..86d28a70240 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -30,7 +30,7 @@ forbiddenfruit==0.1.4 # via blockbuster freezegun==1.5.5 # via -r requirements/test-common.in -idna==3.13 +idna==3.14 # via trustme iniconfig==2.3.0 # via pytest diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index b0ec679c219..53060fdece3 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -51,7 +51,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==26.0.0 # via -r requirements/base-ft.in -idna==3.13 +idna==3.14 # via # trustme # yarl diff --git a/requirements/test.txt b/requirements/test.txt index 95054660a7a..9e4f5485bd2 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -51,7 +51,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==26.0.0 # via -r requirements/base.in -idna==3.13 +idna==3.14 # via # trustme # yarl From f71545480af3b131daab76cee378d8f9e39b8eb7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 17:31:03 +0000 Subject: [PATCH 064/191] Bump certifi from 2026.2.25 to 2026.4.22 (#12506) Bumps [certifi](https://github.com/certifi/python-certifi) from 2026.2.25 to 2026.4.22.
Commits
  • 5dddfb0 2026.04.22 (#410)
  • f99eccd Bump peter-evans/create-pull-request from 8.1.0 to 8.1.1 (#404)
  • 918bed0 Bump actions/upload-artifact from 7.0.0 to 7.0.1 (#405)
  • 0a49067 Bump pypa/gh-action-pypi-publish from 1.13.0 to 1.14.0 (#403)
  • acf6ce8 Bump actions/download-artifact from 8.0.0 to 8.0.1 (#398)
  • feb0ed2 Bump actions/download-artifact from 7.0.0 to 8.0.0 (#397)
  • d9c11a5 Bump actions/upload-artifact from 6.0.0 to 7.0.0 (#396)
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=certifi&package-manager=pip&previous-version=2026.2.25&new-version=2026.4.22)](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/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 41006350232..6000c37e529 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -40,7 +40,7 @@ brotli==1.2.0 ; platform_python_implementation == "CPython" # via -r requirements/runtime-deps.in build==1.4.0 # via pip-tools -certifi==2026.2.25 +certifi==2026.4.22 # via requests cffi==2.0.0 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index b3403b8d3e0..327c68b32a3 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -40,7 +40,7 @@ brotli==1.2.0 ; platform_python_implementation == "CPython" # via -r requirements/runtime-deps.in build==1.4.0 # via pip-tools -certifi==2026.2.25 +certifi==2026.4.22 # via requests cffi==2.0.0 # via diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index ad7435ac2a9..5948a790e98 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -10,7 +10,7 @@ alabaster==1.0.0 # via sphinx babel==2.18.0 # via sphinx -certifi==2026.2.25 +certifi==2026.4.22 # via requests charset-normalizer==3.4.7 # via requests diff --git a/requirements/doc.txt b/requirements/doc.txt index e1843cbbe1d..b3a8e3812b9 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -10,7 +10,7 @@ alabaster==1.0.0 # via sphinx babel==2.18.0 # via sphinx -certifi==2026.2.25 +certifi==2026.4.22 # via requests charset-normalizer==3.4.7 # via requests From a0d188e4e77bbc7b7f3ef4b950edef7bebae7423 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 17:38:20 +0000 Subject: [PATCH 065/191] Bump pydantic from 2.13.3 to 2.13.4 (#12509) 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.3 to 2.13.4.
Release notes

Sourced from pydantic's releases.

v2.13.4 2026-05-06

v2.13.4 (2026-05-06)

What's Changed

Packaging

Fixes

Full Changelog: https://github.com/pydantic/pydantic/compare/v2.13.3...v2.13.4

Changelog

Sourced from pydantic's changelog.

v2.13.4 (2026-05-06)

GitHub release

What's Changed

Packaging

Fixes

Commits
  • cf67d4b Fix linting
  • f0d8a21 Prepare release v2.13.4
  • 5e3fe1d Check for pydantic tag pattern in CI
  • 7f9edcc Document tagging conventions
  • b46a0c9 Adapt pydantic-core linker flags on macOS
  • 50629c8 Update to PyPy 7.3.22
  • 8522ebb Preserve RootModel core metadata
  • a37f3af Adapt MISSING sentinel test to work with unreleased typing_extensions ver...
  • 909259a Remove Logfire example in documentation
  • 2c4174c Bump libc from 0.2.155 to 0.2.185
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pydantic&package-manager=pip&previous-version=2.13.3&new-version=2.13.4)](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 6000c37e529..7d8f69621c5 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -162,9 +162,9 @@ pycares==4.11.0 # via aiodns pycparser==3.0 # via cffi -pydantic==2.13.3 +pydantic==2.13.4 # via python-on-whales -pydantic-core==2.46.3 +pydantic-core==2.46.4 # via pydantic pyenchant==3.3.0 # via sphinxcontrib-spelling diff --git a/requirements/dev.txt b/requirements/dev.txt index 327c68b32a3..417d711801f 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -159,9 +159,9 @@ pycares==4.11.0 # via aiodns pycparser==3.0 # via cffi -pydantic==2.13.3 +pydantic==2.13.4 # via python-on-whales -pydantic-core==2.46.3 +pydantic-core==2.46.4 # via pydantic pygments==2.20.0 # via diff --git a/requirements/lint.txt b/requirements/lint.txt index e3c548c117f..e717e642f86 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -75,9 +75,9 @@ pycares==4.11.0 # via aiodns pycparser==3.0 # via cffi -pydantic==2.13.3 +pydantic==2.13.4 # via python-on-whales -pydantic-core==2.46.3 +pydantic-core==2.46.4 # via pydantic pygments==2.20.0 # via diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 86d28a70240..7b7b72325b5 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -60,9 +60,9 @@ proxy-py==2.4.10 # via -r requirements/test-common.in pycparser==3.0 # via cffi -pydantic==2.13.3 +pydantic==2.13.4 # via python-on-whales -pydantic-core==2.46.3 +pydantic-core==2.46.4 # via pydantic pygments==2.20.0 # via diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 53060fdece3..b5994bf5f20 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -95,9 +95,9 @@ pycares==4.11.0 # via aiodns pycparser==3.0 # via cffi -pydantic==2.13.3 +pydantic==2.13.4 # via python-on-whales -pydantic-core==2.46.3 +pydantic-core==2.46.4 # via pydantic pygments==2.20.0 # via diff --git a/requirements/test.txt b/requirements/test.txt index 9e4f5485bd2..fbcee9e02fe 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -95,9 +95,9 @@ pycares==4.11.0 # via aiodns pycparser==3.0 # via cffi -pydantic==2.13.3 +pydantic==2.13.4 # via python-on-whales -pydantic-core==2.46.3 +pydantic-core==2.46.4 # via pydantic pygments==2.20.0 # via From d4474a81517b3c9f6a1aa3b8373820d5cf97403c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 17:41:27 +0000 Subject: [PATCH 066/191] Bump pkgconfig from 1.5.5 to 1.6.0 (#12511) Bumps [pkgconfig](https://github.com/matze/pkgconfig) from 1.5.5 to 1.6.0.
Release notes

Sourced from pkgconfig's releases.

v1.6.0

Changes see there: https://github.com/matze/pkgconfig?tab=readme-ov-file#changelog

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pkgconfig&package-manager=pip&previous-version=1.5.5&new-version=1.6.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 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.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 7d8f69621c5..5c3127c00e3 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -140,7 +140,7 @@ pathspec==1.1.1 # via mypy pip-tools==7.5.3 # via -r requirements/dev.in -pkgconfig==1.5.5 +pkgconfig==1.6.0 # via -r requirements/test-common.in platformdirs==4.9.6 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index 417d711801f..48db9ec0cc4 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -137,7 +137,7 @@ pathspec==1.1.1 # via mypy pip-tools==7.5.3 # via -r requirements/dev.in -pkgconfig==1.5.5 +pkgconfig==1.6.0 # via -r requirements/test-common.in platformdirs==4.9.6 # via diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 7b7b72325b5..c354df011a1 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -50,7 +50,7 @@ packaging==26.2 # via pytest pathspec==1.1.1 # via mypy -pkgconfig==1.5.5 +pkgconfig==1.6.0 # via -r requirements/test-common.in pluggy==1.6.0 # via diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index b5994bf5f20..2cdd63c9fa4 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -79,7 +79,7 @@ packaging==26.2 # pytest pathspec==1.1.1 # via mypy -pkgconfig==1.5.5 +pkgconfig==1.6.0 # via -r requirements/test-common.in pluggy==1.6.0 # via diff --git a/requirements/test.txt b/requirements/test.txt index fbcee9e02fe..95818452a3b 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -79,7 +79,7 @@ packaging==26.2 # pytest pathspec==1.1.1 # via mypy -pkgconfig==1.5.5 +pkgconfig==1.6.0 # via -r requirements/test-common.in pluggy==1.6.0 # via From 9108cef4700467ce8fa6d98a1e2195a3a420affe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 17:44:25 +0000 Subject: [PATCH 067/191] Bump propcache from 0.4.1 to 0.5.2 (#12508) Bumps [propcache](https://github.com/aio-libs/propcache) from 0.4.1 to 0.5.2.
Release notes

Sourced from propcache's releases.

0.5.2

0.5.0 and 0.5.1 were tagged earlier today but never reached PyPI: 0.5.0's deploy failed at cibuildwheel's post-build pytest on free-threaded armv7l musllinux (SIGBUS under QEMU emulation while importing the C extension), and 0.5.1's deploy hit a transient sigstore Rekor 502 during the attestation step. 0.5.2 is the first of the three to actually publish.

Features

  • Added support for newer type hints and remove Optional and Union from all annotations -- by :user:Vizonex

    Related issues and pull requests on GitHub: #193.

Removals and backward incompatible breaking changes

  • Dropped support for Python 3.9 as it has reached end of life.

    Related issues and pull requests on GitHub: #216.

Packaging updates and notes for downstreams

  • Changed the Cython build dependency from ~= 3.1.0 to >= 3.2.0, removing the upper version bound to avoid conflicts for downstream packagers -- by :user:jameshilliard and :user:gundalow.

    The upstream Cython version is pinned to 3.2.4 in the CI/CD environment.

    Related issues and pull requests on GitHub: #184, #188, #214.

  • Start building and shipping riscv64 wheels -- by :user:justeph.

    Related issues and pull requests on GitHub: #194.

  • The :pep:517 build backend now supports a new build-inplace config setting (and PROPCACHE_BUILD_INPLACE environment variable) for controlling whether to build the project in-tree or in a temporary directory. It only affects wheels and is set up to build in a temporary directory by default. It does not affect editable wheel builds; they will keep being built in-tree regardless.

    Here's an example of using this setting:

    .. code-block:: console

... (truncated)

Changelog

Sourced from propcache's changelog.

0.5.2

(2026-05-08)

No significant changes.


0.5.1

(2026-05-08)

No significant changes.


0.5.0

(2026-05-08)

Features

  • Added support for newer type hints and remove Optional and Union from all annotations -- by :user:Vizonex

    Related issues and pull requests on GitHub: :issue:193.

Removals and backward incompatible breaking changes

  • Dropped support for Python 3.9 as it has reached end of life.

    Related issues and pull requests on GitHub: :issue:216.

Packaging updates and notes for downstreams

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=propcache&package-manager=pip&previous-version=0.4.1&new-version=0.5.2)](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/base-ft.txt | 2 +- requirements/base.txt | 2 +- requirements/constraints.txt | 2 +- requirements/dev.txt | 2 +- requirements/runtime-deps.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt index 0374faf854b..5a3f5931cff 100644 --- a/requirements/base-ft.txt +++ b/requirements/base-ft.txt @@ -34,7 +34,7 @@ multidict==6.7.1 # yarl packaging==26.2 # via gunicorn -propcache==0.4.1 +propcache==0.5.2 # via # -r requirements/runtime-deps.in # yarl diff --git a/requirements/base.txt b/requirements/base.txt index bd1e85fa517..901d436e736 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -34,7 +34,7 @@ multidict==6.7.1 # yarl packaging==26.2 # via gunicorn -propcache==0.4.1 +propcache==0.5.2 # via # -r requirements/runtime-deps.in # yarl diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 5c3127c00e3..74cdbb93f93 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -152,7 +152,7 @@ pluggy==1.6.0 # pytest-cov pre-commit==4.6.0 # via -r requirements/lint.in -propcache==0.4.1 +propcache==0.5.2 # via # -r requirements/runtime-deps.in # yarl diff --git a/requirements/dev.txt b/requirements/dev.txt index 48db9ec0cc4..c1fd0c7499d 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -149,7 +149,7 @@ pluggy==1.6.0 # pytest-cov pre-commit==4.6.0 # via -r requirements/lint.in -propcache==0.4.1 +propcache==0.5.2 # via # -r requirements/runtime-deps.in # yarl diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index 54928256f50..5ec563bdf8a 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -30,7 +30,7 @@ multidict==6.7.1 # via # -r requirements/runtime-deps.in # yarl -propcache==0.4.1 +propcache==0.5.2 # via # -r requirements/runtime-deps.in # yarl diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 2cdd63c9fa4..0e9e527fba3 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -85,7 +85,7 @@ pluggy==1.6.0 # via # pytest # pytest-cov -propcache==0.4.1 +propcache==0.5.2 # via # -r requirements/runtime-deps.in # yarl diff --git a/requirements/test.txt b/requirements/test.txt index 95818452a3b..95948604613 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -85,7 +85,7 @@ pluggy==1.6.0 # via # pytest # pytest-cov -propcache==0.4.1 +propcache==0.5.2 # via # -r requirements/runtime-deps.in # yarl From c2d7192c94aca90aea53e976f254146527ce8b05 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 17:07:45 +0000 Subject: [PATCH 068/191] Bump requests from 2.33.1 to 2.34.0 (#12515) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [requests](https://github.com/psf/requests) from 2.33.1 to 2.34.0.
Release notes

Sourced from requests's releases.

v2.34.0

2.34.0 (2026-05-11)

Announcements

  • Requests 2.34.0 introduces inline types, replacing those provided by typeshed. Public API types should be fully compatible with mypy, pyright, and ty. We believe types are comprehensive but if you find issues, please report them to the pinned tracking issue.

    Special thanks to @​bastimeyer, @​cthoyt, @​edgarrmondragon, and @​srittau for helping review and test the types ahead of the release. (#7272)

Improvements

  • Digest Auth hashing algorithms have added usedforsecurity=False to clarify security considerations. (#7310)
  • Requests added support for Python 3.15 based on beta1. Downstream projects should be able to start testing prior to its release in October. (#7422)
  • Requests added support for Python 3.14t. (#7419)

Bugfixes

  • Response.history no longer contains a reference to itself, preventing accidental looping when traversing the history list. (#7328)
  • Requests no longer performs greedy matching on no_proxy domains. The proxy_bypass implementation has been updated with CPython's fix from bpo-39057. (#7427)
  • Requests no longer incorrectly strips duplicate leading slashes in URI paths. This should address user issues with specific presigned URLs. Note the full fix requires urllib3 2.7.0+. (#7315)

New Contributors

Full Changelog: https://github.com/psf/requests/blob/main/HISTORY.md#2340-2026-05-11

Changelog

Sourced from requests's changelog.

2.34.0 (2026-05-11)

Announcements

  • Requests 2.34.0 introduces inline types, replacing those provided by typeshed. Public API types should be fully compatible with mypy, pyright, and ty. We believe types are comprehensive but if you find issues, please report them to the pinned tracking issue.

    Special thanks to @​bastimeyer, @​cthoyt, @​edgarrmondragon, and @​srittau for helping review and test the types ahead of the release. (#7272)

Improvements

  • Digest Auth hashing algorithms have added usedforsecurity=False to clarify security considerations. (#7310)
  • Requests added support for Python 3.15 based on beta1. Downstream projects should be able to start testing prior to its release in October. (#7422)
  • Requests added support for Python 3.14t. (#7419)

Bugfixes

  • Response.history no longer contains a reference to itself, preventing accidental looping when traversing the history list. (#7328)
  • Requests no longer performs greedy matching on no_proxy domains. The proxy_bypass implementation has been updated with CPython's fix from bpo-39057. (#7427)
  • Requests no longer incorrectly strips duplicate leading slashes in URI paths. This should address user issues with specific presigned URLs. Note the full fix requires urllib3 2.7.0+. (#7315)
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=requests&package-manager=pip&previous-version=2.33.1&new-version=2.34.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 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 74cdbb93f93..629a4f6e569 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -211,7 +211,7 @@ re-assert==1.1.0 # via -r requirements/test-common.in regex==2026.2.28 # via re-assert -requests==2.33.1 +requests==2.34.0 # via # sphinx # sphinxcontrib-spelling diff --git a/requirements/dev.txt b/requirements/dev.txt index c1fd0c7499d..2abd49ce456 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -206,7 +206,7 @@ re-assert==1.1.0 # via -r requirements/test-common.in regex==2026.2.28 # via re-assert -requests==2.33.1 +requests==2.34.0 # via sphinx rich==15.0.0 # via pytest-codspeed diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index 5948a790e98..3817798839b 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -34,7 +34,7 @@ pyenchant==3.3.0 # via sphinxcontrib-spelling pygments==2.20.0 # via sphinx -requests==2.33.1 +requests==2.34.0 # via # sphinx # sphinxcontrib-spelling diff --git a/requirements/doc.txt b/requirements/doc.txt index b3a8e3812b9..ed90f60b969 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -32,7 +32,7 @@ packaging==26.2 # via sphinx pygments==2.20.0 # via sphinx -requests==2.33.1 +requests==2.34.0 # via sphinx snowballstemmer==3.0.1 # via sphinx From d766caab7927ed39798dfc7804d8d72df9111aba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 17:30:36 +0000 Subject: [PATCH 069/191] Bump coverage from 7.13.5 to 7.14.0 (#12518) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.13.5 to 7.14.0.
Changelog

Sourced from coverage's changelog.

Version 7.14.0 — 2026-05-10

  • Feature: now when running one of the reporting commands, if there are parallel data files that need combining, they will be implicitly combined before creating the report. There is no option to avoid the combination; let us know if you have a use case that requires it. Thanks, Tim Hatch <pull 2162_>. Closes issue 1781.

  • Fix: the output from combine was too verbose, listing each file considered. Now it shows a single line with the counts of files combined, files skipped, and files with errors. The -q flag suppresses this line. The old detailed lines are available with the new --debug=combine option.

  • Fix: running a Python file through a symlink now sets the sys.path correctly, matching regular Python behavior. Fixes issue 2157_.

  • Fix: Collector.flush_data could fail with "RuntimeError: Set changed size during iteration" when a tracer in another thread added a line to the per-file set that add_lines (or add_arcs) was iterating. The values passed to CoverageData are now snapshotted via dict.copy() and set.copy(), which are atomic under the GIL. Thanks, Alex Vandiver <pull 2165_>_.

  • Fix: the soft keyword lazy is now bolded in HTML reports.

  • We are no longer testing eventlet support. Eventlet started issuing stern deprecation warnings that break our tests. Our support code is still there.

.. _issue 1781: coveragepy/coveragepy#1781 .. _issue 2157: coveragepy/coveragepy#2157 .. _pull 2162: coveragepy/coveragepy#2162 .. _pull 2165: coveragepy/coveragepy#2165

.. _changes_7-13-5:

Commits
  • 646351b docs: sample HTML for 7.14.0
  • 39cd015 docs: prep for 7.14.0
  • 649e8aa docs: thanks Alex Vandiver for #2165
  • 8cd392e fix: snapshot data in Collector.flush_data to avoid threading race (#2165)
  • c48e0ed fix: less output for combining
  • c2a3a28 docs: explain the change from #2162
  • 1cd47aa fix: implicit combine-during-report now removes the combined data files
  • 2d99fd7 feat: automatically combine coverage in report, thanks Tim Hatch (#2162)
  • 9fbdcdf fix: lazy soft keywords are bolded
  • 5de7d02 build: oops, misplaced quote
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=coverage&package-manager=pip&previous-version=7.13.5&new-version=7.14.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 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.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 629a4f6e569..84fa5f30ad8 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -57,7 +57,7 @@ click==8.3.3 # slotscheck # towncrier # wait-for-it -coverage==7.13.5 +coverage==7.14.0 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/dev.txt b/requirements/dev.txt index 2abd49ce456..08f5841ea61 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -57,7 +57,7 @@ click==8.3.3 # slotscheck # towncrier # wait-for-it -coverage==7.13.5 +coverage==7.14.0 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test-common.txt b/requirements/test-common.txt index c354df011a1..de3560d0672 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -16,7 +16,7 @@ cffi==2.0.0 # pytest-codspeed click==8.3.3 # via wait-for-it -coverage==7.13.5 +coverage==7.14.0 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 0e9e527fba3..fb8c5f4df55 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -31,7 +31,7 @@ cffi==2.0.0 # pytest-codspeed click==8.3.3 # via wait-for-it -coverage==7.13.5 +coverage==7.14.0 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test.txt b/requirements/test.txt index 95948604613..c9c9091f827 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -31,7 +31,7 @@ cffi==2.0.0 # pytest-codspeed click==8.3.3 # via wait-for-it -coverage==7.13.5 +coverage==7.14.0 # via # -r requirements/test-common.in # pytest-cov From aad2c91e20529e06123f03104a4530f3e1db79a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 17:44:58 +0000 Subject: [PATCH 070/191] Bump markdown-it-py from 4.0.0 to 4.2.0 (#12520) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [markdown-it-py](https://github.com/executablebooks/markdown-it-py) from 4.0.0 to 4.2.0.
Release notes

Sourced from markdown-it-py's releases.

v4.2.0

What's Changed

Full Changelog: https://github.com/executablebooks/markdown-it-py/compare/v4.1.0...v4.2.0

v4.1.0

What's Changed

New Contributors

Full Changelog: https://github.com/executablebooks/markdown-it-py/compare/v4.0.0...v4.1.0

Changelog

Sourced from markdown-it-py's changelog.

4.2.0 - 2026-05-07

  • ✨ Add make_fence_rule() factory for configurable fence markers in #394

4.1.0 - 2025-05-06

  • ✨ Add gfm-like2 preset with task lists, alerts, and single-tilde strikethrough core plugins in #388
  • ✨ Allow plugins to register inline terminator characters in #391
  • 👌 Fix quadratic complexity in fragments_join / text_join in #389, thanks to @​petricevich
  • 👌 Add --stdin option to CLI for reading Markdown from standard input in #379, thanks to @​mcepl
  • 🔧 Add typing to Scanner in #382, thanks to @​Alunderin

Full Changelog: https://github.com/executablebooks/markdown-it-py/compare/v4.0.0...v4.1.0

Commits
  • 36c5f54 🚀 RELEASE v4.2.0 (#395)
  • 96cf077 ✨ Add make_fence_rule() factory for configurable fence markers (#394)
  • 3b4ff6d 🚀 RELEASE v4.1.0 (#393)
  • 8951f26 🔧 Update pre-commit hooks (#392)
  • 693bb24 ✨ Add gfm-like2 preset with task lists, alerts, and single-tilde strikethro...
  • df6fd36 ✨Allow plugins to register inline terminator characters (#391)
  • d4ea0ca 👌 Fix quadratic complexity in fragments_join / text_join (#389)
  • 8933147 🔧 Add typing to Scanner (#382)
  • 2f6ae10 🔧 Add AGENTS.md and copilot-setup-steps workflow (#380)
  • 49043e4 Add --stdin option to CLI for reading Markdown from standard input (#379)
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=markdown-it-py&package-manager=pip&previous-version=4.0.0&new-version=4.2.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 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-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 84fa5f30ad8..ee94cdb4afb 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -110,7 +110,7 @@ jinja2==3.1.6 # towncrier librt==0.10.0 # via mypy -markdown-it-py==4.0.0 +markdown-it-py==4.2.0 # via rich markupsafe==3.0.3 # via jinja2 diff --git a/requirements/dev.txt b/requirements/dev.txt index 08f5841ea61..80a685ae1d8 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -108,7 +108,7 @@ jinja2==3.1.6 # towncrier librt==0.10.0 # via mypy -markdown-it-py==4.0.0 +markdown-it-py==4.2.0 # via rich markupsafe==3.0.3 # via jinja2 diff --git a/requirements/lint.txt b/requirements/lint.txt index e717e642f86..bdba9591100 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -49,7 +49,7 @@ isal==1.7.2 # via -r requirements/lint.in librt==0.10.0 # via mypy -markdown-it-py==4.0.0 +markdown-it-py==4.2.0 # via rich mdurl==0.1.2 # via markdown-it-py diff --git a/requirements/test-common.txt b/requirements/test-common.txt index de3560d0672..7ab95c58174 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -38,7 +38,7 @@ isal==1.8.0 ; python_version < "3.14" # via -r requirements/test-common.in librt==0.10.0 # via mypy -markdown-it-py==4.0.0 +markdown-it-py==4.2.0 # via rich mdurl==0.1.2 # via markdown-it-py diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index fb8c5f4df55..6e5609d00b3 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -61,7 +61,7 @@ isal==1.8.0 ; python_version < "3.14" # via -r requirements/test-common.in librt==0.10.0 # via mypy -markdown-it-py==4.0.0 +markdown-it-py==4.2.0 # via rich mdurl==0.1.2 # via markdown-it-py diff --git a/requirements/test.txt b/requirements/test.txt index c9c9091f827..810b7716cbf 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -61,7 +61,7 @@ isal==1.7.2 ; python_version < "3.14" # via -r requirements/test-common.in librt==0.10.0 # via mypy -markdown-it-py==4.0.0 +markdown-it-py==4.2.0 # via rich mdurl==0.1.2 # via markdown-it-py From 2aedb8e7bfeeca977a46c0de3299013e90f6fdd6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 17:58:36 +0000 Subject: [PATCH 071/191] Bump build from 1.4.0 to 1.5.0 (#12522) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [build](https://github.com/pypa/build) from 1.4.0 to 1.5.0.
Release notes

Sourced from build's releases.

1.5.0

What's Changed

Full Changelog: https://github.com/pypa/build/compare/1.4.4...1.5.0

1.4.4

What's Changed

Full Changelog: https://github.com/pypa/build/compare/1.4.3...1.4.4

1.4.3

What's Changed

... (truncated)

Changelog

Sourced from build's changelog.

#################### 1.5.0 (2026-04-30) ####################


Features


  • Drop Python 3.9 support - by :user:henryiii (:issue:1036)

Bugfixes


  • Make --ignore-installed opt-in from the API via fresh=True - by :user:henryiii (:issue:1056)

Miscellaneous


  • :issue:1033

#################### 1.4.4 (2026-04-22) ####################


Bugfixes


  • Fix release pipeline generating CHANGELOG.rst entries with inconsistent heading levels, which broke sphinx -W and pinned Read the Docs stable at 1.4.0 - by :user:gaborbernat. (:issue:1031)
  • Revert :pr:1039 from build 1.4.3, no longer check direct_url (for now) - by :user:henryiii (:issue:1039)
  • Add --ignore-installed to pip install command to prevent issues with packages already present in the isolated build environment - by :user:henryiii (:issue:1037) (:issue:1040)
  • Partial revert of :pr:973, keeping log messages in one entry, multiple lines. (:issue:1044)

Miscellaneous


  • :issue:1048, :issue:1049

#################### 1.4.3 (2026-04-10) ####################


Features


... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=build&package-manager=pip&previous-version=1.4.0&new-version=1.5.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 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 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index ee94cdb4afb..dc34de8137b 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -38,7 +38,7 @@ blockbuster==1.5.26 # -r requirements/test-common.in brotli==1.2.0 ; platform_python_implementation == "CPython" # via -r requirements/runtime-deps.in -build==1.4.0 +build==1.5.0 # via pip-tools certifi==2026.4.22 # via requests diff --git a/requirements/dev.txt b/requirements/dev.txt index 80a685ae1d8..eeb894fa8fa 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -38,7 +38,7 @@ blockbuster==1.5.26 # -r requirements/test-common.in brotli==1.2.0 ; platform_python_implementation == "CPython" # via -r requirements/runtime-deps.in -build==1.4.0 +build==1.5.0 # via pip-tools certifi==2026.4.22 # via requests From 00e20b1487cdf3d1a45df2b4f07d03942345b848 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 17:58:54 +0000 Subject: [PATCH 072/191] Bump virtualenv from 21.3.1 to 21.3.2 (#12521) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [virtualenv](https://github.com/pypa/virtualenv) from 21.3.1 to 21.3.2.
Release notes

Sourced from virtualenv's releases.

21.3.2

What's Changed

Full Changelog: https://github.com/pypa/virtualenv/compare/21.3.1...21.3.2

Changelog

Sourced from virtualenv's changelog.

################# Release History #################

.. towncrier-draft-entries:: [UNRELEASED DRAFT]

.. towncrier release notes start


v21.3.2 (2026-05-12)


No significant changes.


v21.3.1 (2026-05-05)


Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=virtualenv&package-manager=pip&previous-version=21.3.1&new-version=21.3.2)](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 dc34de8137b..8ba96fc89d4 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -287,7 +287,7 @@ uvloop==0.21.0 ; platform_system != "Windows" # -r requirements/lint.in valkey==6.1.1 # via -r requirements/lint.in -virtualenv==21.3.1 +virtualenv==21.3.2 # via pre-commit wait-for-it==2.3.0 # via -r requirements/test-common.in diff --git a/requirements/dev.txt b/requirements/dev.txt index eeb894fa8fa..28029d1b62b 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -277,7 +277,7 @@ uvloop==0.21.0 ; platform_system != "Windows" and implementation_name == "cpytho # -r requirements/lint.in valkey==6.1.1 # via -r requirements/lint.in -virtualenv==21.3.1 +virtualenv==21.3.2 # via pre-commit wait-for-it==2.3.0 # via -r requirements/test-common.in diff --git a/requirements/lint.txt b/requirements/lint.txt index bdba9591100..1d73174b5d4 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -129,7 +129,7 @@ uvloop==0.21.0 ; platform_system != "Windows" # via -r requirements/lint.in valkey==6.1.1 # via -r requirements/lint.in -virtualenv==21.3.1 +virtualenv==21.3.2 # via pre-commit zlib-ng==1.0.0 # via -r requirements/lint.in From b0b0e097d21985d864f7b736f8002a9edb69a45c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 18:06:47 +0000 Subject: [PATCH 073/191] Bump regex from 2026.2.28 to 2026.5.9 (#12507) Bumps [regex](https://github.com/mrabarnett/mrab-regex) from 2026.2.28 to 2026.5.9.
Changelog

Sourced from regex's changelog.

Version: 2026.5.9

Reverse matching with full unicode casefolding could lead to
out-of-range string indexes.

Version: 2026.4.4

A fix for older Python versions before free-threading was
supported.

Version: 2026.4.3

More fixes for free-threading.

Version: 2026.3.32

Fixed segfault.

Version: 2026.3.31

Fixed bug again.

Version: 2026.3.30

Fixed bug.

Version: 2026.3.28

Fixed version.

Version: 2026.3.27

Various fixes, including ones to improve free-threading
support.

Version: 2026.2.28

Replaced atomic operations with mutex on pattern object for
free-threaded Python.

Version: 2026.2.26

PR
[#598](https://github.com/mrabarnett/mrab-regex/issues/598): Fix race
condition in storage caching with atomic operations.

Replaced use of PyUnicode_GET_LENGTH with PyUnicode_GetLength.

Version: 2026.2.19

Added \z as alias of \Z, like in re module.

Added prefixmatch as alias of match, like in re module.

Version: 2026.1.15

... (truncated)

Commits
  • e57d185 Reverse matching with full unicode casefolding lead to out-of-range string in...
  • bc57b04 A fix for older Python versions before free-threading was supported.
  • 773e213 More fixes for free-threading.
  • 5d51c75 Fixed segfault.
  • 2aff2db Fixed bug again.
  • 16af8ae Fixed bug.
  • 2356563 Fixed bug.
  • f579e8f Fixed version.
  • 55315a0 Fixed version.
  • 923d78e Various fixes, including ones to improve free-threading support.
  • See full diff in compare view

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.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 8ba96fc89d4..2c055a9927e 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -209,7 +209,7 @@ pyyaml==6.0.3 # via pre-commit re-assert==1.1.0 # via -r requirements/test-common.in -regex==2026.2.28 +regex==2026.5.9 # via re-assert requests==2.34.0 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index 28029d1b62b..bcd6d3e2c3a 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -204,7 +204,7 @@ pyyaml==6.0.3 # via pre-commit re-assert==1.1.0 # via -r requirements/test-common.in -regex==2026.2.28 +regex==2026.5.9 # via re-assert requests==2.34.0 # via sphinx diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 7ab95c58174..f157accaadd 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -89,7 +89,7 @@ python-on-whales==0.81.0 # via -r requirements/test-common.in re-assert==1.1.0 # via -r requirements/test-common.in -regex==2026.2.28 +regex==2026.5.9 # via re-assert rich==15.0.0 # via pytest-codspeed diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 6e5609d00b3..03b8ab3498c 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -124,7 +124,7 @@ python-on-whales==0.81.0 # via -r requirements/test-common.in re-assert==1.1.0 # via -r requirements/test-common.in -regex==2026.2.28 +regex==2026.5.9 # via re-assert rich==15.0.0 # via pytest-codspeed diff --git a/requirements/test.txt b/requirements/test.txt index 810b7716cbf..69c79767a29 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -124,7 +124,7 @@ python-on-whales==0.81.0 # via -r requirements/test-common.in re-assert==1.1.0 # via -r requirements/test-common.in -regex==2026.2.28 +regex==2026.5.9 # via re-assert rich==15.0.0 # via pytest-codspeed From a551dbccb2e242024d01b13c5f42015a98612097 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 18:12:27 +0000 Subject: [PATCH 074/191] Bump mypy from 2.0.0 to 2.1.0 (#12519) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mypy](https://github.com/python/mypy) from 2.0.0 to 2.1.0.
Changelog

Sourced from mypy's changelog.

Mypy Release Notes

Next Release

Mypy 2.1

We’ve just uploaded mypy 2.1.0 to the Python Package Index (PyPI). Mypy is a static type checker for Python. This release includes new features, performance improvements and bug fixes. You can install it as follows:

python3 -m pip install -U mypy

You can read the full documentation for this release on Read the Docs.

librt.vecs: Fast Growable Array Type for Mypyc

The new librt.vecs module provides an efficient growable array type vec that is optimized for mypyc use. It provides fast, packed arrays with integer and floating point value types, which can be several times faster than list, and tens of times faster than array.array in code compiled using mypyc. It also supports nested vec objects and non-value-type items, such as vec[vec[str]].

Refer to the documentation for the details.

Contributed by Jukka Lehtosalo.

librt.random: Fast Pseudo-Random Number Generation

The new librt.random module provides fast pseudo-random number generation that is optimized for code compiled using mypyc. It can be 3x to 10x faster than the stdlib random module in compiled code.

Refer to the documentation for the details.

Contributed by Jukka Lehtosalo (PR 21433).

Mypyc Improvements

  • Make compilation order with multiple files consistent (Piotr Sawicki, PR 21419)
  • Fix crash on accessing StopAsyncIteration (Piotr Sawicki, PR 21406)
  • Fix incremental compilation with separate flag (Vaggelis Danias, PR 21299)

Fixes to Crashes

  • Fix crash on partial type with --allow-redefinition and global declaration (Jukka Lehtosalo, PR 21428)
  • Fix broken awaitable generator patching (Ivan Levkivskyi, PR 21435)

Changes to Messages

... (truncated)

Commits

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 2c055a9927e..a9c87bdf559 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -108,7 +108,7 @@ jinja2==3.1.6 # via # sphinx # towncrier -librt==0.10.0 +librt==0.11.0 # via mypy markdown-it-py==4.2.0 # via rich @@ -121,7 +121,7 @@ multidict==6.7.1 # -r requirements/multidict.in # -r requirements/runtime-deps.in # yarl -mypy==2.0.0 ; implementation_name == "cpython" +mypy==2.1.0 ; implementation_name == "cpython" # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/dev.txt b/requirements/dev.txt index bcd6d3e2c3a..2bb442d04d5 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -106,7 +106,7 @@ jinja2==3.1.6 # via # sphinx # towncrier -librt==0.10.0 +librt==0.11.0 # via mypy markdown-it-py==4.2.0 # via rich @@ -118,7 +118,7 @@ multidict==6.7.1 # via # -r requirements/runtime-deps.in # yarl -mypy==2.0.0 ; implementation_name == "cpython" +mypy==2.1.0 ; implementation_name == "cpython" # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/lint.txt b/requirements/lint.txt index 1d73174b5d4..7bc393389a4 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -47,13 +47,13 @@ iniconfig==2.3.0 # via pytest isal==1.7.2 # via -r requirements/lint.in -librt==0.10.0 +librt==0.11.0 # via mypy markdown-it-py==4.2.0 # via rich mdurl==0.1.2 # via markdown-it-py -mypy==2.0.0 ; implementation_name == "cpython" +mypy==2.1.0 ; implementation_name == "cpython" # via -r requirements/lint.in mypy-extensions==1.1.0 # via mypy diff --git a/requirements/test-common.txt b/requirements/test-common.txt index f157accaadd..e3c5d6b7c41 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -36,13 +36,13 @@ iniconfig==2.3.0 # via pytest isal==1.8.0 ; python_version < "3.14" # via -r requirements/test-common.in -librt==0.10.0 +librt==0.11.0 # via mypy markdown-it-py==4.2.0 # via rich mdurl==0.1.2 # via markdown-it-py -mypy==2.0.0 ; implementation_name == "cpython" +mypy==2.1.0 ; implementation_name == "cpython" # via -r requirements/test-common.in mypy-extensions==1.1.0 # via mypy diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 03b8ab3498c..d38d630a88d 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -59,7 +59,7 @@ iniconfig==2.3.0 # via pytest isal==1.8.0 ; python_version < "3.14" # via -r requirements/test-common.in -librt==0.10.0 +librt==0.11.0 # via mypy markdown-it-py==4.2.0 # via rich @@ -69,7 +69,7 @@ multidict==6.7.1 # via # -r requirements/runtime-deps.in # yarl -mypy==2.0.0 ; implementation_name == "cpython" +mypy==2.1.0 ; implementation_name == "cpython" # via -r requirements/test-common.in mypy-extensions==1.1.0 # via mypy diff --git a/requirements/test.txt b/requirements/test.txt index 69c79767a29..3c4b0917287 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -59,7 +59,7 @@ iniconfig==2.3.0 # via pytest isal==1.7.2 ; python_version < "3.14" # via -r requirements/test-common.in -librt==0.10.0 +librt==0.11.0 # via mypy markdown-it-py==4.2.0 # via rich @@ -69,7 +69,7 @@ multidict==6.7.1 # via # -r requirements/runtime-deps.in # yarl -mypy==2.0.0 ; implementation_name == "cpython" +mypy==2.1.0 ; implementation_name == "cpython" # via -r requirements/test-common.in mypy-extensions==1.1.0 # via mypy From 9f1b9883f9dfac953431fca61e2870d886670346 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 01:05:01 +0000 Subject: [PATCH 075/191] Bump identify from 2.6.17 to 2.6.19 (#12510) Bumps [identify](https://github.com/pre-commit/identify) from 2.6.17 to 2.6.19.
Commits
  • b39f637 v2.6.19
  • c976888 Merge pull request #588 from hofbi/patch-1
  • 6110d73 Add support for 'tif' file extension
  • ccbd337 Merge pull request #587 from pre-commit/pre-commit-ci-update-config
  • f5af264 [pre-commit.ci] pre-commit autoupdate
  • a0be598 Merge pull request #586 from pre-commit/pre-commit-ci-update-config
  • b184043 [pre-commit.ci] pre-commit autoupdate
  • 07a8017 v2.6.18
  • 2609c0a Merge pull request #581 from pre-commit/mxr-patch-1
  • 74d7931 Configure pyproject.toml to have custom 'pyproject' file type
  • See full diff in compare view

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 a9c87bdf559..e07b9b3a4a5 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -89,7 +89,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==26.0.0 # via -r requirements/base.in -identify==2.6.17 +identify==2.6.19 # via pre-commit idna==3.14 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index 2bb442d04d5..bb57abdd2de 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -87,7 +87,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==26.0.0 # via -r requirements/base.in -identify==2.6.17 +identify==2.6.19 # via pre-commit idna==3.14 # via diff --git a/requirements/lint.txt b/requirements/lint.txt index 7bc393389a4..63dd740c911 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -39,7 +39,7 @@ forbiddenfruit==0.1.4 # via blockbuster freezegun==1.5.5 # via -r requirements/lint.in -identify==2.6.17 +identify==2.6.19 # via pre-commit idna==3.14 # via trustme From 8acd7c4876f0c3fcff7ccc4158f7b9f976637c1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 11:40:11 +0000 Subject: [PATCH 076/191] Bump pytest-codspeed from 4.5.0 to 5.0.1 (#12529) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pytest-codspeed](https://github.com/CodSpeedHQ/pytest-codspeed) from 4.5.0 to 5.0.1.
Release notes

Sourced from pytest-codspeed's releases.

v5.0.1

What's Changed

Full Changelog: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v5.0.0...v5.0.1

v5.0.0

Highlights

MacOS walltime profiling is now available with the codspeed cli v4.16.1 and above.

pytest-codspeed can now be used in free threaded mode. This has been tested with 3.14t and 3.15t. For this, we have dropped usage of cffi in favor of the native extension support.

What's Changed

Full Changelog: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v4.5.0...v5.0.0

Changelog

Sourced from pytest-codspeed's changelog.

[5.0.1] - 2026-05-13

💼 Other

[5.0.0] - 2026-05-13

🚀 Features

⚡ Performance

  • Bind callgrind start/stop directly to avoid extra frame by @​art049 in #96

⚙️ Internals

Commits
  • d4d9dc6 Release v5.0.1 🚀
  • ad709a5 build: enable free-threaded wheels in cibuildwheel (#121)
  • 080d620 Release v5.0.0 🚀
  • befdebf chore: ignore common compilation warnings for instrument-hooks
  • ee98055 chore: use unsigned bash in the macos test
  • 5a205c8 feat: use instrument_hooks markers in walltime
  • fda1fbc feat(hooks): declare native extension free-thread safe (#120)
  • f3ed388 perf(hooks): bind callgrind start/stop directly to avoid extra frame
  • e4a419e chore: bump pinned uv version to 0.11.14
  • ee07afb ci: add python 3.15 and 3.15t to test matrix
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pytest-codspeed&package-manager=pip&previous-version=4.5.0&new-version=5.0.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 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 | 3 +-- requirements/dev.txt | 3 +-- requirements/lint.txt | 3 +-- requirements/test-common.txt | 6 ++---- requirements/test-ft.txt | 3 +-- requirements/test.txt | 3 +-- 6 files changed, 7 insertions(+), 14 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index e07b9b3a4a5..30251b5d3df 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -46,7 +46,6 @@ cffi==2.0.0 # via # cryptography # pycares - # pytest-codspeed cfgv==3.5.0 # via pre-commit charset-normalizer==3.4.7 @@ -185,7 +184,7 @@ pytest==9.0.3 # pytest-cov # pytest-mock # pytest-xdist -pytest-codspeed==4.5.0 +pytest-codspeed==5.0.1 # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/dev.txt b/requirements/dev.txt index bb57abdd2de..23d0c3618b5 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -46,7 +46,6 @@ cffi==2.0.0 # via # cryptography # pycares - # pytest-codspeed cfgv==3.5.0 # via pre-commit charset-normalizer==3.4.7 @@ -180,7 +179,7 @@ pytest==9.0.3 # pytest-cov # pytest-mock # pytest-xdist -pytest-codspeed==4.5.0 +pytest-codspeed==5.0.1 # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/lint.txt b/requirements/lint.txt index 63dd740c911..77bc593f283 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -20,7 +20,6 @@ cffi==2.0.0 # via # cryptography # pycares - # pytest-codspeed cfgv==3.5.0 # via pre-commit click==8.3.3 @@ -88,7 +87,7 @@ pytest==9.0.3 # -r requirements/lint.in # pytest-codspeed # pytest-mock -pytest-codspeed==4.5.0 +pytest-codspeed==5.0.1 # via -r requirements/lint.in pytest-mock==3.15.1 # via -r requirements/lint.in diff --git a/requirements/test-common.txt b/requirements/test-common.txt index e3c5d6b7c41..8d1ed8401ba 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -11,9 +11,7 @@ ast-serialize==0.3.0 blockbuster==1.5.26 # via -r requirements/test-common.in cffi==2.0.0 - # via - # cryptography - # pytest-codspeed + # via cryptography click==8.3.3 # via wait-for-it coverage==7.14.0 @@ -75,7 +73,7 @@ pytest==9.0.3 # pytest-cov # pytest-mock # pytest-xdist -pytest-codspeed==4.5.0 +pytest-codspeed==5.0.1 # via -r requirements/test-common.in pytest-cov==7.1.0 # via -r requirements/test-common.in diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index d38d630a88d..43e29aeee45 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -28,7 +28,6 @@ cffi==2.0.0 # via # cryptography # pycares - # pytest-codspeed click==8.3.3 # via wait-for-it coverage==7.14.0 @@ -110,7 +109,7 @@ pytest==9.0.3 # pytest-cov # pytest-mock # pytest-xdist -pytest-codspeed==4.5.0 +pytest-codspeed==5.0.1 # via -r requirements/test-common.in pytest-cov==7.1.0 # via -r requirements/test-common.in diff --git a/requirements/test.txt b/requirements/test.txt index 3c4b0917287..96fc50138c1 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -28,7 +28,6 @@ cffi==2.0.0 # via # cryptography # pycares - # pytest-codspeed click==8.3.3 # via wait-for-it coverage==7.14.0 @@ -110,7 +109,7 @@ pytest==9.0.3 # pytest-cov # pytest-mock # pytest-xdist -pytest-codspeed==4.5.0 +pytest-codspeed==5.0.1 # via -r requirements/test-common.in pytest-cov==7.1.0 # via -r requirements/test-common.in From 7b8cbab4d5f3e788c205abae2596ce18f5e347f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 18:13:18 +0000 Subject: [PATCH 077/191] Bump idna from 3.14 to 3.15 (#12530) Bumps [idna](https://github.com/kjd/idna) from 3.14 to 3.15.
Changelog

Sourced from idna's changelog.

3.15 (2026-05-12)

  • Enforce DNS-length cap on individual labels early in check_label, short-circuiting contextual-rule processing for oversized input while staying compatible with UTS 46 usage.
  • Tidy core helpers: hoist bidi category sets to module-level frozensets (avoiding per-codepoint list construction), simplify length checks, and reuse the shared _unicode_dots_re from idna.core in the codec module.
  • Use raise ... from err for proper exception chaining and switch internal string formatting to f-strings.
  • Allow flit_core 4.x in the build backend.
  • Expand the ruff lint set (flake8-bugbear, flake8-simplify, pyupgrade, perflint) and apply the surfaced fixes; pin lint CI to Python 3.14.
  • Add Dependabot configuration for GitHub Actions.
  • Convert README and HISTORY from reStructuredText to Markdown.
  • Reference CVE-2026-45409 for the 3.14 advisory in place of the initial GHSA identifier.

Thanks to Felix Yan, Stan Ulbrych, and metsw24-max for contributions to this release.

Commits
  • af30a09 Release 3.15
  • 30314d4 Pre-release 3.15rc0
  • 05d4b21 Merge pull request #237 from kjd/convert-docs-to-markdown
  • 2987fdb Convert README and HISTORY from reStructuredText to Markdown
  • 59fa800 Merge pull request #236 from kjd/dependabot/github_actions/actions-f3e34333ea
  • def6983 Merge branch 'master' into dependabot/github_actions/actions-f3e34333ea
  • bbd8004 Merge pull request #234 from StanFromIreland/patch-1
  • edd07c0 Bump github/codeql-action from 3.35.2 to 4.35.2 in the actions group
  • 5557db0 Merge branch 'master' into patch-1
  • f11746c Merge pull request #235 from StanFromIreland/patch-2
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=idna&package-manager=pip&previous-version=3.14&new-version=3.15)](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/base-ft.txt | 2 +- requirements/base.txt | 2 +- requirements/constraints.txt | 2 +- requirements/dev.txt | 2 +- requirements/doc-spelling.txt | 2 +- requirements/doc.txt | 2 +- requirements/lint.txt | 2 +- requirements/runtime-deps.txt | 2 +- requirements/test-common.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt index 5a3f5931cff..4997512fad4 100644 --- a/requirements/base-ft.txt +++ b/requirements/base-ft.txt @@ -26,7 +26,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==26.0.0 # via -r requirements/base-ft.in -idna==3.14 +idna==3.15 # via yarl multidict==6.7.1 # via diff --git a/requirements/base.txt b/requirements/base.txt index 901d436e736..c62f33b577e 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -26,7 +26,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==26.0.0 # via -r requirements/base.in -idna==3.14 +idna==3.15 # via yarl multidict==6.7.1 # via diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 30251b5d3df..2afed24f06d 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -90,7 +90,7 @@ gunicorn==26.0.0 # via -r requirements/base.in identify==2.6.19 # via pre-commit -idna==3.14 +idna==3.15 # via # requests # trustme diff --git a/requirements/dev.txt b/requirements/dev.txt index 23d0c3618b5..6e7943731a9 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -88,7 +88,7 @@ gunicorn==26.0.0 # via -r requirements/base.in identify==2.6.19 # via pre-commit -idna==3.14 +idna==3.15 # via # requests # trustme diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index 3817798839b..e83817f8bc2 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -18,7 +18,7 @@ click==8.3.3 # via towncrier docutils==0.21.2 # via sphinx -idna==3.14 +idna==3.15 # via requests imagesize==1.5.0 # via sphinx diff --git a/requirements/doc.txt b/requirements/doc.txt index ed90f60b969..d66b2d07989 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -18,7 +18,7 @@ click==8.3.3 # via towncrier docutils==0.21.2 # via sphinx -idna==3.14 +idna==3.15 # via requests imagesize==1.5.0 # via sphinx diff --git a/requirements/lint.txt b/requirements/lint.txt index 77bc593f283..c02e3891d1d 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -40,7 +40,7 @@ freezegun==1.5.5 # via -r requirements/lint.in identify==2.6.19 # via pre-commit -idna==3.14 +idna==3.15 # via trustme iniconfig==2.3.0 # via pytest diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index 5ec563bdf8a..05fb65c337a 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -24,7 +24,7 @@ frozenlist==1.8.0 # via # -r requirements/runtime-deps.in # aiosignal -idna==3.14 +idna==3.15 # via yarl multidict==6.7.1 # via diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 8d1ed8401ba..9cb3e3314e3 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -28,7 +28,7 @@ forbiddenfruit==0.1.4 # via blockbuster freezegun==1.5.5 # via -r requirements/test-common.in -idna==3.14 +idna==3.15 # via trustme iniconfig==2.3.0 # via pytest diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 43e29aeee45..3b707adccbb 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -50,7 +50,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==26.0.0 # via -r requirements/base-ft.in -idna==3.14 +idna==3.15 # via # trustme # yarl diff --git a/requirements/test.txt b/requirements/test.txt index 96fc50138c1..e8f13fd1986 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -50,7 +50,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==26.0.0 # via -r requirements/base.in -idna==3.14 +idna==3.15 # via # trustme # yarl From e4d7411c2e5c5f7af466de090f07eaf2921c03ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 18:13:52 +0000 Subject: [PATCH 078/191] Bump python-discovery from 1.3.0 to 1.3.1 (#12528) 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.3.0 to 1.3.1.
Release notes

Sourced from python-discovery's releases.

v1.3.1

What's Changed

New Contributors

Full Changelog: https://github.com/tox-dev/python-discovery/compare/1.3.0...1.3.1

Changelog

Sourced from python-discovery's changelog.

Bug fixes - 1.3.1

  • export normalize_isa and deprecate KNOWN_ARCHITECTURES - by :user:rahuldevikar. (:issue:59)
  • discover uv-managed Pythons on Windows. Previously the glob assumed Unix layout (<root>/<key>/bin/python) and silently found nothing on Windows, where uv places python.exe directly under the install root - by :user:gaborbernat. (:issue:65)
  • Canonicalize GraalVM to match GraalPy Python interpreter in PythonSpec and PythonInfo. - by :user:timfel. (:issue:73)

v1.3.0 (2026-05-05)


Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=python-discovery&package-manager=pip&previous-version=1.3.0&new-version=1.3.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 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 2afed24f06d..3d09edb1f5e 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -198,7 +198,7 @@ pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 # via freezegun -python-discovery==1.3.0 +python-discovery==1.3.1 # via virtualenv python-on-whales==0.81.0 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index 6e7943731a9..7fa0003ee9d 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -193,7 +193,7 @@ pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 # via freezegun -python-discovery==1.3.0 +python-discovery==1.3.1 # via virtualenv python-on-whales==0.81.0 # via diff --git a/requirements/lint.txt b/requirements/lint.txt index c02e3891d1d..7a74bdffe84 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -93,7 +93,7 @@ pytest-mock==3.15.1 # via -r requirements/lint.in python-dateutil==2.9.0.post0 # via freezegun -python-discovery==1.3.0 +python-discovery==1.3.1 # via virtualenv python-on-whales==0.81.0 # via -r requirements/lint.in From 8c40e4ea8e6ef4dbb472564baf1cc1d6b6f056b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 18:14:25 +0000 Subject: [PATCH 079/191] Bump imagesize from 1.5.0 to 2.0.0 (#12525) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [//]: # (dependabot-start) ⚠️ **Dependabot is rebasing this PR** ⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Bumps [imagesize](https://github.com/shibukawa/imagesize_py) from 1.5.0 to 2.0.0.
Commits
  • 5ab28d4 bump module version to 2.0
  • 63d6afb Merge pull request #82 from shibukawa/codex/update-readme-and-setup-instructi...
  • 2946066 docs: clarify EXIF orientation formats in v2.0 notes
  • 53eff2e Merge pull request #81 from shibukawa/codex/refactor-code-to-reduce-duplication
  • ac14f2a Refactor duplicated JPEG segment parsing logic
  • 48ab954 Merge pull request #80 from shibukawa/codex/add-avif-exif-rotation-support
  • 5cada10 Add AVIF EXIF rotation support
  • 232c6d5 Merge pull request #79 from shibukawa/codex/add-heic/heif-support-and-rotation
  • 324c970 Add HEIC/HEIF size and rotation support
  • 7b7bb5f Merge pull request #78 from shibukawa/codex/add-pypi-link-and-python-version-...
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=imagesize&package-manager=pip&previous-version=1.5.0&new-version=2.0.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 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 3d09edb1f5e..3af35076bde 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -95,7 +95,7 @@ idna==3.15 # requests # trustme # yarl -imagesize==1.5.0 +imagesize==2.0.0 # via sphinx iniconfig==2.3.0 # via pytest diff --git a/requirements/dev.txt b/requirements/dev.txt index 7fa0003ee9d..9c7796fa8a3 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -93,7 +93,7 @@ idna==3.15 # requests # trustme # yarl -imagesize==1.5.0 +imagesize==2.0.0 # via sphinx iniconfig==2.3.0 # via pytest diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index e83817f8bc2..20aa6b758be 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -20,7 +20,7 @@ docutils==0.21.2 # via sphinx idna==3.15 # via requests -imagesize==1.5.0 +imagesize==2.0.0 # via sphinx jinja2==3.1.6 # via diff --git a/requirements/doc.txt b/requirements/doc.txt index d66b2d07989..07019543c0b 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -20,7 +20,7 @@ docutils==0.21.2 # via sphinx idna==3.15 # via requests -imagesize==1.5.0 +imagesize==2.0.0 # via sphinx jinja2==3.1.6 # via From 6d09be73c36bfe553db7b60740e09360b54055d1 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 19:24:00 +0000 Subject: [PATCH 080/191] [PR #12395/7eb0e802 backport][3.14] Fix CookieError crash with control characters on CPython builds with CVE-2026-3644 patch (#12533) **This is a backport of PR #12395 as merged into master (7eb0e8029cdb270a98a203bc5d4bad32cdc2c19c).** Co-authored-by: Rodrigo Nogueira --- CHANGES/12395.bugfix.rst | 4 ++ aiohttp/_cookie_helpers.py | 62 +++++++++++++------ tests/test_cookie_helpers.py | 117 ++++++++++++++++++++++++++++++++++- 3 files changed, 161 insertions(+), 22 deletions(-) create mode 100644 CHANGES/12395.bugfix.rst diff --git a/CHANGES/12395.bugfix.rst b/CHANGES/12395.bugfix.rst new file mode 100644 index 00000000000..e3c67bfa7aa --- /dev/null +++ b/CHANGES/12395.bugfix.rst @@ -0,0 +1,4 @@ +Fixed a crash (:external+python:exc:`~http.cookies.CookieError`) in the cookie parser when receiving cookies +containing ASCII control characters on CPython builds with the :cve:`2026-3644` +patch. The parser now gracefully skips cookies whose value contains control +characters instead of letting the exception propagate -- by :user:`rodrigobnogueira`. diff --git a/aiohttp/_cookie_helpers.py b/aiohttp/_cookie_helpers.py index aca86b7f771..00ca820768e 100644 --- a/aiohttp/_cookie_helpers.py +++ b/aiohttp/_cookie_helpers.py @@ -7,7 +7,7 @@ import re from collections.abc import Sequence -from http.cookies import Morsel +from http.cookies import CookieError, Morsel from typing import cast from .log import internal_logger @@ -106,9 +106,16 @@ def preserve_morsel_with_coded_value(cookie: Morsel[str]) -> Morsel[str]: # bypass validation and set already validated state. This is more stable than # setting protected attributes directly and unlikely to change since it would # break pickling. - mrsl_val.__setstate__( # type: ignore[attr-defined] - {"key": cookie.key, "value": cookie.value, "coded_value": cookie.coded_value} - ) + try: + mrsl_val.__setstate__( # type: ignore[attr-defined] + { + "key": cookie.key, + "value": cookie.value, + "coded_value": cookie.coded_value, + } + ) + except CookieError: + return cookie return mrsl_val @@ -206,10 +213,18 @@ def parse_cookie_header(header: str) -> list[tuple[str, Morsel[str]]]: invalid_names.append(key) else: morsel = Morsel() - morsel.__setstate__( # type: ignore[attr-defined] - {"key": key, "value": _unquote(value), "coded_value": value} - ) - cookies.append((key, morsel)) + try: + morsel.__setstate__( # type: ignore[attr-defined] + { + "key": key, + "value": _unquote(value), + "coded_value": value, + } + ) + except CookieError: + pass + else: + cookies.append((key, morsel)) # Move to next cookie or end i = next_semi + 1 if next_semi != -1 else n @@ -231,9 +246,12 @@ def parse_cookie_header(header: str) -> list[tuple[str, Morsel[str]]]: # bypass validation and set already validated state. This is more stable than # setting protected attributes directly and unlikely to change since it would # break pickling. - morsel.__setstate__( # type: ignore[attr-defined] - {"key": key, "value": _unquote(value), "coded_value": value} - ) + try: + morsel.__setstate__( # type: ignore[attr-defined] + {"key": key, "value": _unquote(value), "coded_value": value} + ) + except CookieError: + continue cookies.append((key, morsel)) @@ -323,15 +341,19 @@ def parse_set_cookie_headers(headers: Sequence[str]) -> list[tuple[str, Morsel[s # Create new morsel current_morsel = Morsel() # Preserve the original value as coded_value (with quotes if present) - # We use __setstate__ instead of the public set() API because it allows us to - # bypass validation and set already validated state. This is more stable than - # setting protected attributes directly and unlikely to change since it would - # break pickling. - current_morsel.__setstate__( # type: ignore[attr-defined] - {"key": key, "value": _unquote(value), "coded_value": value} - ) - parsed_cookies.append((key, current_morsel)) - morsel_seen = True + try: + current_morsel.__setstate__( # type: ignore[attr-defined] + { + "key": key, + "value": _unquote(value), + "coded_value": value, + } + ) + except CookieError: + current_morsel = None + else: + parsed_cookies.append((key, current_morsel)) + morsel_seen = True else: # Invalid cookie string - no value for non-attribute break diff --git a/tests/test_cookie_helpers.py b/tests/test_cookie_helpers.py index fead869d6f3..be6228d698f 100644 --- a/tests/test_cookie_helpers.py +++ b/tests/test_cookie_helpers.py @@ -1134,6 +1134,65 @@ def test_parse_set_cookie_headers_uses_unquote_with_octal( assert morsel.coded_value == expected_coded +@pytest.mark.parametrize( + ("header", "expected_name", "expected_coded"), + [ + pytest.param( + r'name="\012newline\012"', + "name", + r'"\012newline\012"', + id="newline-octal-012", + ), + pytest.param( + r'tab="\011separated\011values"', + "tab", + r'"\011separated\011values"', + id="tab-octal-011", + ), + ], +) +def test_parse_set_cookie_headers_ctl_chars_from_octal( + header: str, expected_name: str, expected_coded: str +) -> None: + """Ensure octal escapes that decode to control characters don't crash the parser. + + CPython builds with the CVE-2026-3644 patch reject control characters in + cookies. When octal unquoting produces a control character, the parser + skips the cookie entirely instead of raising CookieError. + """ + result = parse_set_cookie_headers([header]) + + # On CPython with CVE-2026-3644 patch the cookie is rejected (result is empty); + # on older builds it may be accepted with the decoded value. + # Either way, no crash. + if result: + name, morsel = result[0] + assert name == expected_name + assert morsel.coded_value == expected_coded + + +def test_parse_set_cookie_headers_literal_ctl_chars() -> None: + r"""Ensure literal control characters in a cookie value don't crash the parser. + + If the raw header itself contains a control character (e.g. BEL \\x07), + both the decoded value and coded_value are unsalvageable. The parser + should gracefully skip the cookie instead of raising CookieError. + """ + result = parse_set_cookie_headers(['name="a\x07b"']) + # On CPython with CVE-2026-3644 patch the cookie is skipped; + # on older builds it may be accepted. Either way, no crash. + if result: + assert result[0][0] == "name" + + +def test_parse_set_cookie_headers_literal_ctl_chars_preserves_others() -> None: + """Ensure a cookie with literal control chars doesn't break subsequent cookies.""" + result = parse_set_cookie_headers(['bad="a\x07b"; good=value', "another=cookie"]) + # "good" is an attribute of "bad" (same header), so it's not a separate cookie. + # "another" is in a separate header and must always be preserved. + assert any(name == "another" for name, _ in result) + + # Tests for parse_cookie_header (RFC 6265 compliant Cookie header parser) @@ -1597,8 +1656,17 @@ def test_parse_cookie_header_empty_key_in_fallback( assert name2 == "another" assert morsel2.value == "test" - assert "Cannot load cookie. Illegal cookie name" in caplog.text - assert "''" in caplog.text + +def test_parse_cookie_header_literal_ctl_chars() -> None: + r"""Ensure literal control characters in a cookie value don't crash the parser. + + If the raw header itself contains a control character (e.g. BEL \\x07), + the cookie is unsalvageable. The parser should gracefully skip it. + """ + result = parse_cookie_header('name="a\x07b"; good=cookie') + # On CPython with CVE-2026-3644 patch the bad cookie is skipped; + # on older builds it may be accepted. Either way, no crash. + assert any(name == "good" for name, _ in result) @pytest.mark.parametrize( @@ -1789,3 +1857,48 @@ def test_unquote_compatibility_with_simplecookie(test_value: str) -> None: f"our={_unquote(test_value)!r}, " f"SimpleCookie={simplecookie_unquote(test_value)!r}" ) + + +@pytest.fixture +def mock_strict_morsel( + monkeypatch: pytest.MonkeyPatch, +) -> None: + original_setstate = Morsel.__setstate__ # type: ignore[attr-defined] + + def _mock_setstate(self: Morsel[str], state: dict[str, str]) -> None: + if any(ord(c) < 32 for c in state.get("value", "")): + raise CookieError() + original_setstate(self, state) + + monkeypatch.setattr( + "aiohttp._cookie_helpers.Morsel.__setstate__", + _mock_setstate, + ) + + +@pytest.mark.usefixtures("mock_strict_morsel") +def test_cookie_helpers_cve_fallback() -> None: + # Clean value: mock delegates to original_setstate → succeeds + m: Morsel[str] = Morsel() + m.__setstate__({"key": "k", "value": "clean", "coded_value": "clean"}) # type: ignore[attr-defined] + assert m.key == "k" + + # With strict morsel: any CTL char in value → CookieError → rejected + with pytest.raises(CookieError): + Morsel().__setstate__( # type: ignore[attr-defined] + {"key": "k", "value": "v\n", "coded_value": "v\\012"} + ) + with pytest.raises(CookieError): + Morsel().__setstate__( # type: ignore[attr-defined] + {"key": "k", "value": "v\n", "coded_value": "v\n"} + ) + + cookie: Morsel[str] = Morsel() + cookie._key, cookie._value, cookie._coded_value = "k", "v\n", "v\n" # type: ignore[attr-defined] + assert preserve_morsel_with_coded_value(cookie) is cookie + + assert parse_cookie_header("f=b\x07r;") == [] + assert parse_cookie_header("f=b\x07r") == [] + assert parse_cookie_header('f="b\x07r";') == [] + assert parse_set_cookie_headers(['f="b\x07r";']) == [] + assert parse_set_cookie_headers([r'name="\012newline\012"']) == [] From ad9baf0b3abf97190e18ffff6dcc0034372bf955 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 13:14:44 +0000 Subject: [PATCH 081/191] Bump requests from 2.34.0 to 2.34.1 (#12539) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [requests](https://github.com/psf/requests) from 2.34.0 to 2.34.1.
Release notes

Sourced from requests's releases.

v2.34.1

2.34.1 (2026-05-13)

Bugfixes

  • Widened json input type from dict and list to Mapping and Sequence. (#7436)
  • Changed headers input type to MutableMapping and removed None from Request.headers typing to improve handling for users. (#7431)
  • Response.reason moved from str | None to str to improve handling for users. (#7437)
  • Fixed a bug where some bodies with custom __getattr__ implementations weren't being properly detected as Iterables. (#7433)

New Contributors

Full Changelog: https://github.com/psf/requests/blob/main/HISTORY.md#2341-2026-05-13

Changelog

Sourced from requests's changelog.

2.34.1 (2026-05-13)

Bugfixes

  • Widened json input type from dict and list to Mapping and Sequence. (#7436)
  • Changed headers input type to MutableMapping and removed None from Request.headers typing to improve handling for users. (#7431)
  • Response.reason moved from str | None to str to improve handling for users. (#7437)
  • Fixed a bug where some bodies with custom __getattr__ implementations weren't being properly detected as Iterables. (#7433)
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=requests&package-manager=pip&previous-version=2.34.0&new-version=2.34.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 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 3af35076bde..2ce0fa22d9c 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -210,7 +210,7 @@ re-assert==1.1.0 # via -r requirements/test-common.in regex==2026.5.9 # via re-assert -requests==2.34.0 +requests==2.34.1 # via # sphinx # sphinxcontrib-spelling diff --git a/requirements/dev.txt b/requirements/dev.txt index 9c7796fa8a3..00b84161e87 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -205,7 +205,7 @@ re-assert==1.1.0 # via -r requirements/test-common.in regex==2026.5.9 # via re-assert -requests==2.34.0 +requests==2.34.1 # via sphinx rich==15.0.0 # via pytest-codspeed diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index 20aa6b758be..415d2024cf8 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -34,7 +34,7 @@ pyenchant==3.3.0 # via sphinxcontrib-spelling pygments==2.20.0 # via sphinx -requests==2.34.0 +requests==2.34.1 # via # sphinx # sphinxcontrib-spelling diff --git a/requirements/doc.txt b/requirements/doc.txt index 07019543c0b..347ddac1e95 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -32,7 +32,7 @@ packaging==26.2 # via sphinx pygments==2.20.0 # via sphinx -requests==2.34.0 +requests==2.34.1 # via sphinx snowballstemmer==3.0.1 # via sphinx From 45ac3c9dbc8129d9fb55ac160b7c8a478c6a5c5d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 14:28:23 +0000 Subject: [PATCH 082/191] Bump virtualenv from 21.3.2 to 21.3.3 (#12538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [virtualenv](https://github.com/pypa/virtualenv) from 21.3.2 to 21.3.3.
Release notes

Sourced from virtualenv's releases.

21.3.3

What's Changed

Full Changelog: https://github.com/pypa/virtualenv/compare/21.3.2...21.3.3

Changelog

Sourced from virtualenv's changelog.

Bugfixes - 21.3.3

  • recognize GraalPy interpreters using the normalized GraalPy name - by :user:timfel. (:issue:3144)

v21.3.2 (2026-05-12)


No significant changes.


v21.3.1 (2026-05-05)


Bugfixes - 21.3.1

  • Upgrade embedded wheels:

    • pip to 26.1.1 from 26.1 (:issue:3138)

v21.3.0 (2026-04-27)


Features - 21.3.0

  • Re-introduce xonsh shell activator (activate.xsh) previously removed in 20.7.0, and make the plugin loader prefer virtualenv's built-in entry points so a third-party package cannot override them by registering a duplicate name. (:issue:3003)

Bugfixes - 21.3.0

  • Upgrade embedded wheels:

    • pip to 26.1 (:issue:3132)

v21.2.4 (2026-04-14)


Bugfixes - 21.2.4

  • Security hardening: validate each entry of a seed wheel archive before extracting it so a tampered wheel cannot escape the app-data image directory via an absolute path or .. traversal. (:issue:3118)
  • Security hardening: verify the SHA-256 of every bundled seed wheel when it is loaded so a corrupted or tampered file

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=virtualenv&package-manager=pip&previous-version=21.3.2&new-version=21.3.3)](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 2ce0fa22d9c..cda8e6e1b17 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -286,7 +286,7 @@ uvloop==0.21.0 ; platform_system != "Windows" # -r requirements/lint.in valkey==6.1.1 # via -r requirements/lint.in -virtualenv==21.3.2 +virtualenv==21.3.3 # via pre-commit wait-for-it==2.3.0 # via -r requirements/test-common.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 00b84161e87..8c0621cd569 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -276,7 +276,7 @@ uvloop==0.21.0 ; platform_system != "Windows" and implementation_name == "cpytho # -r requirements/lint.in valkey==6.1.1 # via -r requirements/lint.in -virtualenv==21.3.2 +virtualenv==21.3.3 # via pre-commit wait-for-it==2.3.0 # via -r requirements/test-common.in diff --git a/requirements/lint.txt b/requirements/lint.txt index 7a74bdffe84..3a68cb3101b 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -128,7 +128,7 @@ uvloop==0.21.0 ; platform_system != "Windows" # via -r requirements/lint.in valkey==6.1.1 # via -r requirements/lint.in -virtualenv==21.3.2 +virtualenv==21.3.3 # via pre-commit zlib-ng==1.0.0 # via -r requirements/lint.in From ccbcb64dcbcdf3dcbf533219c3a724f014413247 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 02:33:58 +0000 Subject: [PATCH 083/191] Bump aiodns from 3.6.1 to 4.0.0 (#11947) Bumps [aiodns](https://github.com/saghul/aiodns) from 3.6.1 to 4.0.0.
Changelog

Sourced from aiodns's changelog.

4.0.0

  • Breaking change: Requires pycares >= 5.0.0
  • Added new query_dns() method returning native pycares 5.x DNSResult types
  • Deprecated query() method - still works with backward-compatible result types
  • Deprecated gethostbyname() method - use getaddrinfo() instead
  • Added compatibility layer for pycares 4.x result types to ease migration
  • Updated dependencies
    • Bumped pycares from 4.11.0 to 5.0.1 (#220)
    • Bumped pytest from 8.4.2 to 9.0.2 (#224)
    • Bumped pytest-asyncio from 1.2.0 to 1.3.0 (#223)
    • Bumped mypy from 1.19.0 to 1.19.1 (#219)
    • Bumped winloop from 0.3.1 to 0.4.0 (#210)
    • Bumped actions/upload-artifact from 5 to 6 (#222)
    • Bumped actions/download-artifact from 6.0.0 to 7.0.0 (#221)
Commits

> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- aiohttp/resolver.py | 7 +++---- requirements/base-ft.txt | 4 ++-- requirements/base.txt | 4 ++-- requirements/constraints.txt | 4 ++-- requirements/dev.txt | 4 ++-- requirements/lint.txt | 4 ++-- requirements/runtime-deps.txt | 4 ++-- requirements/test-ft.txt | 4 ++-- requirements/test.txt | 4 ++-- 9 files changed, 19 insertions(+), 20 deletions(-) diff --git a/aiohttp/resolver.py b/aiohttp/resolver.py index 85c36da0c31..84b5ffb667e 100644 --- a/aiohttp/resolver.py +++ b/aiohttp/resolver.py @@ -127,8 +127,7 @@ async def resolve( hosts: list[ResolveResult] = [] for node in resp.nodes: address: tuple[bytes, int] | tuple[bytes, int, int, int] = node.addr - family = node.family - if family == socket.AF_INET6: + if node.family == socket.AF_INET6: if len(address) > 3 and address[3]: # This is essential for link-local IPv6 addresses. # LL IPv6 is a VERY rare case. Strictly speaking, we should use @@ -142,7 +141,7 @@ async def resolve( resolved_host = address[0].decode("ascii") port = address[1] else: # IPv4 - assert family == socket.AF_INET + assert node.family == socket.AF_INET resolved_host = address[0].decode("ascii") port = address[1] hosts.append( @@ -150,7 +149,7 @@ async def resolve( hostname=host, host=resolved_host, port=port, - family=family, + family=node.family, proto=0, flags=_NUMERIC_SOCKET_FLAGS, ) diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt index 4997512fad4..10fa8f02117 100644 --- a/requirements/base-ft.txt +++ b/requirements/base-ft.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/base-ft.txt --strip-extras requirements/base-ft.in # -aiodns==3.6.1 +aiodns==4.0.0 # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.1 # via -r requirements/runtime-deps.in @@ -38,7 +38,7 @@ propcache==0.5.2 # via # -r requirements/runtime-deps.in # yarl -pycares==4.11.0 +pycares==5.0.1 # via aiodns pycparser==3.0 # via cffi diff --git a/requirements/base.txt b/requirements/base.txt index c62f33b577e..20730032d38 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/base.txt --strip-extras requirements/base.in # -aiodns==3.6.1 +aiodns==4.0.0 # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.1 # via -r requirements/runtime-deps.in @@ -38,7 +38,7 @@ propcache==0.5.2 # via # -r requirements/runtime-deps.in # yarl -pycares==4.11.0 +pycares==5.0.1 # via aiodns pycparser==3.0 # via cffi diff --git a/requirements/constraints.txt b/requirements/constraints.txt index cda8e6e1b17..2553d26bc91 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/constraints.txt --strip-extras requirements/constraints.in # -aiodns==3.6.1 +aiodns==4.0.0 # via # -r requirements/lint.in # -r requirements/runtime-deps.in @@ -157,7 +157,7 @@ propcache==0.5.2 # yarl proxy-py==2.4.10 # via -r requirements/test-common.in -pycares==4.11.0 +pycares==5.0.1 # via aiodns pycparser==3.0 # via cffi diff --git a/requirements/dev.txt b/requirements/dev.txt index 8c0621cd569..d90bd2671fe 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/dev.txt --strip-extras requirements/dev.in # -aiodns==3.6.1 +aiodns==4.0.0 # via # -r requirements/lint.in # -r requirements/runtime-deps.in @@ -154,7 +154,7 @@ propcache==0.5.2 # yarl proxy-py==2.4.10 # via -r requirements/test-common.in -pycares==4.11.0 +pycares==5.0.1 # via aiodns pycparser==3.0 # via cffi diff --git a/requirements/lint.txt b/requirements/lint.txt index 3a68cb3101b..15db6efe0b0 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/lint.txt --resolver=backtracking --strip-extras requirements/lint.in # -aiodns==3.6.1 +aiodns==4.0.0 # via -r requirements/lint.in annotated-types==0.7.0 # via pydantic @@ -70,7 +70,7 @@ pluggy==1.6.0 # via pytest pre-commit==4.6.0 # via -r requirements/lint.in -pycares==4.11.0 +pycares==5.0.1 # via aiodns pycparser==3.0 # via cffi diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index 05fb65c337a..df3530154c3 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/runtime-deps.txt --strip-extras requirements/runtime-deps.in # -aiodns==3.6.1 +aiodns==4.0.0 # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.1 # via -r requirements/runtime-deps.in @@ -34,7 +34,7 @@ propcache==0.5.2 # via # -r requirements/runtime-deps.in # yarl -pycares==4.11.0 +pycares==5.0.1 # via aiodns pycparser==3.0 # via cffi diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 3b707adccbb..4b6022c47cf 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/test-ft.txt --strip-extras requirements/test-ft.in # -aiodns==3.6.1 +aiodns==4.0.0 # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.1 # via -r requirements/runtime-deps.in @@ -90,7 +90,7 @@ propcache==0.5.2 # yarl proxy-py==2.4.10 # via -r requirements/test-common.in -pycares==4.11.0 +pycares==5.0.1 # via aiodns pycparser==3.0 # via cffi diff --git a/requirements/test.txt b/requirements/test.txt index e8f13fd1986..d05424534b9 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/test.txt --strip-extras requirements/test.in # -aiodns==3.6.1 +aiodns==4.0.0 # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.1 # via -r requirements/runtime-deps.in @@ -90,7 +90,7 @@ propcache==0.5.2 # yarl proxy-py==2.4.10 # via -r requirements/test-common.in -pycares==4.11.0 +pycares==5.0.1 # via aiodns pycparser==3.0 # via cffi From 554d8af54ab4e34ac4c8e1e5107ed747e1580d0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 11:27:53 +0000 Subject: [PATCH 084/191] Bump requests from 2.34.1 to 2.34.2 (#12546) Bumps [requests](https://github.com/psf/requests) from 2.34.1 to 2.34.2.
Release notes

Sourced from requests's releases.

v2.34.2

2.34.2 (2026-05-14)

  • Moved headers input type back to Mapping to avoid invariance issues with MutableMapping and inferred dict types. Users calling Request.headers.update() may need to narrow typing in their code. (#7441)

Full Changelog: https://github.com/psf/requests/blob/main/HISTORY.md#2342-2026-05-14

Changelog

Sourced from requests's changelog.

2.34.2 (2026-05-14)

  • Moved headers input type back to Mapping to avoid invariance issues with MutableMapping and inferred dict types. Users calling Request.headers.update() may need to narrow typing in their code. (#7441)
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=requests&package-manager=pip&previous-version=2.34.1&new-version=2.34.2)](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/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 2553d26bc91..50a0807def8 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -210,7 +210,7 @@ re-assert==1.1.0 # via -r requirements/test-common.in regex==2026.5.9 # via re-assert -requests==2.34.1 +requests==2.34.2 # via # sphinx # sphinxcontrib-spelling diff --git a/requirements/dev.txt b/requirements/dev.txt index d90bd2671fe..9487db895b6 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -205,7 +205,7 @@ re-assert==1.1.0 # via -r requirements/test-common.in regex==2026.5.9 # via re-assert -requests==2.34.1 +requests==2.34.2 # via sphinx rich==15.0.0 # via pytest-codspeed diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index 415d2024cf8..ed58e6c876f 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -34,7 +34,7 @@ pyenchant==3.3.0 # via sphinxcontrib-spelling pygments==2.20.0 # via sphinx -requests==2.34.1 +requests==2.34.2 # via # sphinx # sphinxcontrib-spelling diff --git a/requirements/doc.txt b/requirements/doc.txt index 347ddac1e95..660c70163e8 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -32,7 +32,7 @@ packaging==26.2 # via sphinx pygments==2.20.0 # via sphinx -requests==2.34.1 +requests==2.34.2 # via sphinx snowballstemmer==3.0.1 # via sphinx From 680cac4c8cffecd343f1ac9f081da5f5c20350ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 11:30:53 +0000 Subject: [PATCH 085/191] Bump pytest-codspeed from 5.0.1 to 5.0.2 (#12545) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [//]: # (dependabot-start) ⚠️ **Dependabot is rebasing this PR** ⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Bumps [pytest-codspeed](https://github.com/CodSpeedHQ/pytest-codspeed) from 5.0.1 to 5.0.2.
Release notes

Sourced from pytest-codspeed's releases.

v5.0.2

What's Changed

Full Changelog: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v5.0.1...v5.0.2

Changelog

Sourced from pytest-codspeed's changelog.

[5.0.2] - 2026-05-14

🚀 Features

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pytest-codspeed&package-manager=pip&previous-version=5.0.1&new-version=5.0.2)](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 +- 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 50a0807def8..e2e39becce8 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -184,7 +184,7 @@ pytest==9.0.3 # pytest-cov # pytest-mock # pytest-xdist -pytest-codspeed==5.0.1 +pytest-codspeed==5.0.2 # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 9487db895b6..236052a9f8d 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -179,7 +179,7 @@ pytest==9.0.3 # pytest-cov # pytest-mock # pytest-xdist -pytest-codspeed==5.0.1 +pytest-codspeed==5.0.2 # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/lint.txt b/requirements/lint.txt index 15db6efe0b0..cc38079bb98 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -87,7 +87,7 @@ pytest==9.0.3 # -r requirements/lint.in # pytest-codspeed # pytest-mock -pytest-codspeed==5.0.1 +pytest-codspeed==5.0.2 # via -r requirements/lint.in pytest-mock==3.15.1 # via -r requirements/lint.in diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 9cb3e3314e3..48a9114ab27 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -73,7 +73,7 @@ pytest==9.0.3 # pytest-cov # pytest-mock # pytest-xdist -pytest-codspeed==5.0.1 +pytest-codspeed==5.0.2 # via -r requirements/test-common.in pytest-cov==7.1.0 # via -r requirements/test-common.in diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 4b6022c47cf..2013c7512c7 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -109,7 +109,7 @@ pytest==9.0.3 # pytest-cov # pytest-mock # pytest-xdist -pytest-codspeed==5.0.1 +pytest-codspeed==5.0.2 # via -r requirements/test-common.in pytest-cov==7.1.0 # via -r requirements/test-common.in diff --git a/requirements/test.txt b/requirements/test.txt index d05424534b9..c3dcb79a2b1 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -109,7 +109,7 @@ pytest==9.0.3 # pytest-cov # pytest-mock # pytest-xdist -pytest-codspeed==5.0.1 +pytest-codspeed==5.0.2 # via -r requirements/test-common.in pytest-cov==7.1.0 # via -r requirements/test-common.in From 8808809e5c5d489cfe70abe52b06d5afb088fecd Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sat, 16 May 2026 17:41:59 +0100 Subject: [PATCH 086/191] Fix unpickler (#12557) --- aiohttp/cookiejar.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/aiohttp/cookiejar.py b/aiohttp/cookiejar.py index a0330ec901a..cd19c9e79cc 100644 --- a/aiohttp/cookiejar.py +++ b/aiohttp/cookiejar.py @@ -39,13 +39,18 @@ _SIMPLE_COOKIE = SimpleCookie() -class _RestrictedCookieUnpickler(pickle.Unpickler): +class _RestrictedCookieUnpickler(pickle._Unpickler): """A restricted unpickler that only allows cookie-related types. This prevents arbitrary code execution when loading pickled cookie data from untrusted sources. Only types that are expected in a serialized CookieJar are permitted. + Subclasses :class:`pickle._Unpickler` (the pure-Python implementation) + rather than :class:`pickle.Unpickler` because the accelerated unpickler + on some implementations (notably PyPy) does not dispatch through + :meth:`find_class` overrides. + See: https://docs.python.org/3/library/pickle.html#restricting-globals """ From 2a76d8e0bddd875139c5163fe48f985281b5ed6d Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 17:30:49 +0000 Subject: [PATCH 087/191] [PR #12436/a868ca9d backport][3.14] Fix digest authentication for URLs with reserved characters (#12558) Co-authored-by: J. Nick Koston --- CHANGES/12436.bugfix.rst | 1 + aiohttp/client_middleware_digest_auth.py | 6 ++- tests/test_client_middleware_digest_auth.py | 41 +++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 CHANGES/12436.bugfix.rst diff --git a/CHANGES/12436.bugfix.rst b/CHANGES/12436.bugfix.rst new file mode 100644 index 00000000000..d6f7e160697 --- /dev/null +++ b/CHANGES/12436.bugfix.rst @@ -0,0 +1 @@ +Fixed digest authentication failing for requests whose path or query string contains percent-encoded reserved characters; the digest signature now uses the encoded request-target that is sent on the wire instead of the decoded form -- by :user:`bdraco`. diff --git a/aiohttp/client_middleware_digest_auth.py b/aiohttp/client_middleware_digest_auth.py index 64257a65c18..8151dea5154 100644 --- a/aiohttp/client_middleware_digest_auth.py +++ b/aiohttp/client_middleware_digest_auth.py @@ -246,7 +246,11 @@ async def _encode(self, method: str, url: URL, body: Payload | Literal[b""]) -> # Convert string values to bytes once nonce_bytes = nonce.encode("utf-8") realm_bytes = realm.encode("utf-8") - path = URL(url).path_qs + # Use the encoded request-target (raw_path_qs) since that is what is + # transmitted on the wire and what the server signs against. Using the + # decoded form would cause digest verification to fail when the path + # or query string contains percent-encoded reserved characters. + path = URL(url).raw_path_qs # Process QoP qop = "" diff --git a/tests/test_client_middleware_digest_auth.py b/tests/test_client_middleware_digest_auth.py index c490fb70d78..0d2d6ad3325 100644 --- a/tests/test_client_middleware_digest_auth.py +++ b/tests/test_client_middleware_digest_auth.py @@ -189,6 +189,47 @@ async def test_encode_digest_with_md5( assert "algorithm=MD5" in header +@pytest.mark.parametrize( + ("url", "expected_uri"), + [ + ( + URL("http://example.com/axis-cgi/io/port.cgi?action=9:\\"), + "/axis-cgi/io/port.cgi?action=9:%5C", + ), + ( + URL("http://example.com/path with space/file"), + "/path%20with%20space/file", + ), + ( + URL("http://example.com/p?q=a&b=1+2"), + "/p?q=a&b=1+2", + ), + ( + URL.build( + scheme="http", + host="example.com", + path="/p", + query={"x": "[]"}, + ), + "/p?x=%5B%5D", + ), + ], + ids=["backslash-and-colon", "space-in-path", "ampersand-and-plus", "brackets"], +) +async def test_encode_uri_uses_wire_encoded_request_target( + auth_mw_with_challenge: DigestAuthMiddleware, + url: URL, + expected_uri: str, +) -> None: + """The digest uri/A2 must use the encoded request-target sent on the wire. + + Servers compute the digest signature against the encoded request-target + they actually receive, so the client must sign the same encoded form. + """ + header = await auth_mw_with_challenge._encode("GET", url, b"") + assert f'uri="{expected_uri}"' in header + + @pytest.mark.parametrize( "algorithm", ["MD5-SESS", "SHA-SESS", "SHA-256-SESS", "SHA-512-SESS"] ) From c4d7bf24a4c40131784a46db1e0575550506f43f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 16 May 2026 11:26:58 -0700 Subject: [PATCH 088/191] [3.14] Fix AppKey repr tests for collections.abc typing module change (#12563) Fixes #12560 --- tests/test_web_app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_web_app.py b/tests/test_web_app.py index d83c292b6d0..21e33ffb277 100644 --- a/tests/test_web_app.py +++ b/tests/test_web_app.py @@ -293,6 +293,7 @@ def test_appkey_repr_nonconcrete() -> None: # pytest-xdist: "", "", + "", ) @@ -309,6 +310,7 @@ def test_appkey_repr_annotated() -> None: # pytest-xdist: "", "", + "", ) From 2c459116859d19bf4ef79f15fea71766291ccb1c Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 13:02:25 -0700 Subject: [PATCH 089/191] [PR #12565/edc1cc2b backport][3.13] Bump benchmark CI job timeout to 15 minutes (#12566) Co-authored-by: aio-libs bot Co-authored-by: Claude Opus 4.7 (1M context) Fixes #12561 --- .github/workflows/ci-cd.yml | 2 +- CHANGES/12561.misc.rst | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 CHANGES/12561.misc.rst diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 5d1a2487c2f..7349fb3930a 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -273,7 +273,7 @@ jobs: if: >- needs.pre-setup.outputs.upstream-repository-id == github.repository_id runs-on: ubuntu-latest - timeout-minutes: 12 + timeout-minutes: 15 steps: - name: Checkout project uses: actions/checkout@v5 diff --git a/CHANGES/12561.misc.rst b/CHANGES/12561.misc.rst new file mode 100644 index 00000000000..4201bb90b0d --- /dev/null +++ b/CHANGES/12561.misc.rst @@ -0,0 +1,2 @@ +Bumped the benchmark CI job timeout from 12 to 15 minutes to prevent +spurious failures on slower runners -- by :user:`aiolibsbot`. From 1474536b8026d34d27feae9c30ee9883a002c4fa Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 13:02:36 -0700 Subject: [PATCH 090/191] [PR #12565/edc1cc2b backport][3.14] Bump benchmark CI job timeout to 15 minutes (#12567) Co-authored-by: aio-libs bot Co-authored-by: Claude Opus 4.7 (1M context) Fixes #12561 --- .github/workflows/ci-cd.yml | 2 +- CHANGES/12561.misc.rst | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 CHANGES/12561.misc.rst diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 52b4fac1e02..cf48ccd6aea 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -366,7 +366,7 @@ jobs: if: >- needs.pre-setup.outputs.upstream-repository-id == github.repository_id runs-on: ubuntu-latest - timeout-minutes: 12 + timeout-minutes: 15 steps: - name: Checkout project uses: actions/checkout@v6 diff --git a/CHANGES/12561.misc.rst b/CHANGES/12561.misc.rst new file mode 100644 index 00000000000..4201bb90b0d --- /dev/null +++ b/CHANGES/12561.misc.rst @@ -0,0 +1,2 @@ +Bumped the benchmark CI job timeout from 12 to 15 minutes to prevent +spurious failures on slower runners -- by :user:`aiolibsbot`. From 2c3daecc6f1dcc74baa1b1720ccdc5c18198f29d Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 13:31:54 -0700 Subject: [PATCH 091/191] [PR #12564/80599593 backport][3.14] ci: report slowest benchmarks via --durations=30 (#12568) Co-authored-by: aio-libs bot Co-authored-by: J. Nick Koston Fixes https://github.com/aio-libs/aiohttp/issues/12562 --- .github/workflows/ci-cd.yml | 2 +- CHANGES/12562.contrib.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 CHANGES/12562.contrib.rst diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index cf48ccd6aea..78953a31855 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -399,7 +399,7 @@ jobs: uses: CodSpeedHQ/action@v4 with: mode: instrumentation - run: python -Im pytest --no-cov -vvvvv --codspeed + run: python -Im pytest --no-cov -vvvvv --codspeed --durations=30 cython-coverage: diff --git a/CHANGES/12562.contrib.rst b/CHANGES/12562.contrib.rst new file mode 100644 index 00000000000..0d89212d365 --- /dev/null +++ b/CHANGES/12562.contrib.rst @@ -0,0 +1 @@ +Added ``--durations=30`` to the benchmark CI run so the slowest tests are reported when the job hits its timeout -- by :user:`aiolibsbot`. From bcea105dbb42d996acef5e35f065f4ba7ceb1531 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 13:40:23 -0700 Subject: [PATCH 092/191] [PR #12321/1aaf43b3 backport][3.14] Add cdef type declarations and inline upgrade check in HTTP parser (#12559) Co-authored-by: J. Nick Koston --- CHANGES/12321.misc.rst | 2 ++ aiohttp/_http_parser.pyx | 6 ++++-- docs/spelling_wordlist.txt | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 CHANGES/12321.misc.rst diff --git a/CHANGES/12321.misc.rst b/CHANGES/12321.misc.rst new file mode 100644 index 00000000000..71f9db77675 --- /dev/null +++ b/CHANGES/12321.misc.rst @@ -0,0 +1,2 @@ +Added ``cdef`` type declarations and inlined the upgrade check in the HTTP parser +-- by :user:`bdraco`. diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index d8bb4f1c39f..146822b4887 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -455,6 +455,9 @@ cdef class HttpParser: self._has_value = True cdef _on_headers_complete(self): + cdef str h_upg + cdef str enc + self._process_header() http_version = self.http_version() @@ -469,8 +472,7 @@ cdef class HttpParser: if http_version == HttpVersion11 and hdrs.HOST not in headers: raise BadHttpMessage("Missing 'Host' header in request.") h_upg = headers.get("upgrade", "") - allowed = upgrade and h_upg.isascii() and h_upg.lower() in ALLOWED_UPGRADES - if allowed or self._cparser.method == cparser.HTTP_CONNECT: + if (upgrade and h_upg.isascii() and h_upg.lower() in ALLOWED_UPGRADES) or self._cparser.method == cparser.HTTP_CONNECT: self._upgraded = True else: if upgrade and self._cparser.status_code == 101: diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 2d39c4a1713..a422b523639 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -155,6 +155,7 @@ Indices infos initializer inline +inlined intaking io IoT From ab68167435c80919efd2e185debed12b7cac0cc8 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 23:54:26 +0000 Subject: [PATCH 093/191] [PR #12571/2a35975f backport][3.14] Fix flakey middleware recursion tests by using server URL (#12575) Co-authored-by: J. Nick Koston --- CHANGES/12571.contrib.rst | 4 ++++ tests/test_client_middleware.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 CHANGES/12571.contrib.rst diff --git a/CHANGES/12571.contrib.rst b/CHANGES/12571.contrib.rst new file mode 100644 index 00000000000..8fb1b309375 --- /dev/null +++ b/CHANGES/12571.contrib.rst @@ -0,0 +1,4 @@ +Fixed two flakey ``test_middleware_uses_session_avoids_recursion_with_*`` tests +that hard coded ``localhost`` in the inner middleware request; they now target +the bound server URL so happy eyeballs cannot pick an unbound address on +Windows runners -- by :user:`bdraco`. diff --git a/tests/test_client_middleware.py b/tests/test_client_middleware.py index da5bcece6e8..384dc354d76 100644 --- a/tests/test_client_middleware.py +++ b/tests/test_client_middleware.py @@ -957,7 +957,7 @@ async def log_middleware( if request.url.path != "/log": # Use the session from the request to make the logging call async with request.session.post( - f"http://localhost:{log_server.port}/log", + log_server.make_url("/log"), json={"method": str(request.method), "url": str(request.url)}, ) as resp: assert resp.status == 200 @@ -1025,7 +1025,7 @@ async def log_middleware( # Use the session from the request to make the logging call # Disable middleware to avoid infinite recursion async with request.session.post( - f"http://localhost:{log_server.port}/log", + log_server.make_url("/log"), json={"method": str(request.method), "url": str(request.url)}, middlewares=(), # This prevents infinite recursion ) as resp: From c59d9a16583c6c8882b76c25ff36538177dbd847 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 19:36:10 -0700 Subject: [PATCH 094/191] [PR #12576/5edd76f0 backport][3.14] Parallelize Cython extension compilation (#12578) Co-authored-by: J. Nick Koston --- CHANGES/12576.packaging.rst | 4 ++++ setup.py | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 CHANGES/12576.packaging.rst diff --git a/CHANGES/12576.packaging.rst b/CHANGES/12576.packaging.rst new file mode 100644 index 00000000000..dc748bfe726 --- /dev/null +++ b/CHANGES/12576.packaging.rst @@ -0,0 +1,4 @@ +Parallelized the Cython extension compilation by defaulting +``build_ext.parallel`` to ``os.cpu_count()``, so each module's +``gcc`` invocation now runs concurrently instead of one at a time +-- by :user:`bdraco`. diff --git a/setup.py b/setup.py index 9f910fa823a..d44c4a595ce 100644 --- a/setup.py +++ b/setup.py @@ -3,6 +3,7 @@ import sys from setuptools import Extension, setup +from setuptools.command.build_ext import build_ext if sys.version_info < (3, 10): raise RuntimeError("aiohttp 3.x requires Python 3.10+") @@ -87,8 +88,19 @@ ] +class ParallelBuildExt(build_ext): + def build_extensions(self) -> None: + if self.parallel is None: + self.parallel = os.cpu_count() or 1 + super().build_extensions() + + build_type = "Pure" if NO_EXTENSIONS else "Accelerated" -setup_kwargs = {} if NO_EXTENSIONS else {"ext_modules": extensions} +setup_kwargs = ( + {} + if NO_EXTENSIONS + else {"ext_modules": extensions, "cmdclass": {"build_ext": ParallelBuildExt}} +) print("*********************", file=sys.stderr) print("* {build_type} build *".format_map(locals()), file=sys.stderr) From 8884d62b7fcf8e926412f63b7fec9de365c4c86a Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 19:52:07 -0700 Subject: [PATCH 095/191] [PR #12576/5edd76f0 backport][3.13] Parallelize Cython extension compilation (#12577) Co-authored-by: J. Nick Koston --- CHANGES/12576.packaging.rst | 4 ++++ setup.py | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 CHANGES/12576.packaging.rst diff --git a/CHANGES/12576.packaging.rst b/CHANGES/12576.packaging.rst new file mode 100644 index 00000000000..dc748bfe726 --- /dev/null +++ b/CHANGES/12576.packaging.rst @@ -0,0 +1,4 @@ +Parallelized the Cython extension compilation by defaulting +``build_ext.parallel`` to ``os.cpu_count()``, so each module's +``gcc`` invocation now runs concurrently instead of one at a time +-- by :user:`bdraco`. diff --git a/setup.py b/setup.py index fafb7dc7941..9006a19a4f6 100644 --- a/setup.py +++ b/setup.py @@ -3,6 +3,7 @@ import sys from setuptools import Extension, setup +from setuptools.command.build_ext import build_ext if sys.version_info < (3, 9): raise RuntimeError("aiohttp 3.x requires Python 3.9+") @@ -70,8 +71,19 @@ ] +class ParallelBuildExt(build_ext): + def build_extensions(self) -> None: + if self.parallel is None: + self.parallel = os.cpu_count() or 1 + super().build_extensions() + + build_type = "Pure" if NO_EXTENSIONS else "Accelerated" -setup_kwargs = {} if NO_EXTENSIONS else {"ext_modules": extensions} +setup_kwargs = ( + {} + if NO_EXTENSIONS + else {"ext_modules": extensions, "cmdclass": {"build_ext": ParallelBuildExt}} +) print("*********************", file=sys.stderr) print("* {build_type} build *".format_map(locals()), file=sys.stderr) From 0303bed8904baae1e6a1a059cf6ffbdbcf229adc Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 19:52:18 -0700 Subject: [PATCH 096/191] [PR #12571/2a35975f backport][3.13] Fix flakey middleware recursion tests by using server URL (#12574) Co-authored-by: J. Nick Koston --- CHANGES/12571.contrib.rst | 4 ++++ tests/test_client_middleware.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 CHANGES/12571.contrib.rst diff --git a/CHANGES/12571.contrib.rst b/CHANGES/12571.contrib.rst new file mode 100644 index 00000000000..8fb1b309375 --- /dev/null +++ b/CHANGES/12571.contrib.rst @@ -0,0 +1,4 @@ +Fixed two flakey ``test_middleware_uses_session_avoids_recursion_with_*`` tests +that hard coded ``localhost`` in the inner middleware request; they now target +the bound server URL so happy eyeballs cannot pick an unbound address on +Windows runners -- by :user:`bdraco`. diff --git a/tests/test_client_middleware.py b/tests/test_client_middleware.py index 217877759c0..6db56a744ca 100644 --- a/tests/test_client_middleware.py +++ b/tests/test_client_middleware.py @@ -957,7 +957,7 @@ async def log_middleware( if request.url.path != "/log": # Use the session from the request to make the logging call async with request.session.post( - f"http://localhost:{log_server.port}/log", + log_server.make_url("/log"), json={"method": str(request.method), "url": str(request.url)}, ) as resp: assert resp.status == 200 @@ -1025,7 +1025,7 @@ async def log_middleware( # Use the session from the request to make the logging call # Disable middleware to avoid infinite recursion async with request.session.post( - f"http://localhost:{log_server.port}/log", + log_server.make_url("/log"), json={"method": str(request.method), "url": str(request.url)}, middlewares=(), # This prevents infinite recursion ) as resp: From 48cab10ea76eb7f91cbccfcd8d067340175a5fbe Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 02:56:48 +0000 Subject: [PATCH 097/191] [PR #12512/637e1265 backport][3.14] Threat model chapter 1 (#12548) Co-authored-by: Sam Bull Co-authored-by: J. Nick Koston --- CHANGES/12512.misc.rst | 1 + THREAT_MODEL.md | 314 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 315 insertions(+) create mode 100644 CHANGES/12512.misc.rst create mode 100644 THREAT_MODEL.md diff --git a/CHANGES/12512.misc.rst b/CHANGES/12512.misc.rst new file mode 100644 index 00000000000..c3acbcce786 --- /dev/null +++ b/CHANGES/12512.misc.rst @@ -0,0 +1 @@ +Added ``THREAT_MODEL.md`` detailing our security stance -- by :user:`Dreamsorcerer`. diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md new file mode 100644 index 00000000000..1c10ce1e219 --- /dev/null +++ b/THREAT_MODEL.md @@ -0,0 +1,314 @@ +# aiohttp Threat Model + +This document is a STRIDE-based threat model for the +[aiohttp](https://github.com/aio-libs/aiohttp) library. It is a living document +intended to (a) make explicit the implicit security assumptions baked into the +codebase, (b) catalogue known classes of threat against each subsystem, and +(c) record the existing and recommended mitigations. + +--- + +## 1. Library Overview + +**aiohttp** is an `asyncio`-based HTTP client/server framework for Python. It +provides: + +- An **HTTP/1.1 server** (`aiohttp.web`) including routing, middleware, + WebSocket support, static-file serving, and a Gunicorn worker. +- An **HTTP/1.1 client** (`aiohttp.ClientSession`) including connection + pooling, TLS, proxy support, redirects, cookie handling, and WebSockets. +- Shared **wire-protocol code**: HTTP/1 parser (vendored + [llhttp](https://github.com/nodejs/llhttp) wrapped in Cython, with a pure + Python fallback), HTTP writer, WebSocket framing, multipart, and compression. + +Key public APIs (non-exhaustive): + +| Surface | Entry points | +| --- | --- | +| Server | `aiohttp.web.Application`, `web.RouteTableDef`, `web.run_app`, `web.AppRunner`, `web.WebSocketResponse`, `web.FileResponse` | +| Client | `aiohttp.ClientSession`, `aiohttp.TCPConnector`, `aiohttp.ClientResponse`, `aiohttp.WSMessage`, `aiohttp.BasicAuth` | +| Shared | `aiohttp.MultipartReader`/`MultipartWriter`, `aiohttp.CookieJar`, `aiohttp.TraceConfig`, `aiohttp.resolver.AsyncResolver` | + +--- + +## 2. Methodology + +We use [STRIDE](https://en.wikipedia.org/wiki/STRIDE_model): + +- **S**poofing — impersonating identity (host, user, peer, dependency). +- **T**ampering — modifying data or code in flight or at rest. +- **R**epudiation — denying that an action occurred. +- **I**nformation Disclosure — leaking confidential data. +- **D**enial of Service — exhausting CPU, memory, sockets, file descriptors. +- **E**levation of Privilege — gaining unintended access. + +Risk is ranked **High / Medium / Low** based on a rough product of likelihood +and impact, as judged by maintainers. Mitigations are split into +**existing** (already implemented in the codebase) and **recommended** (not +yet implemented or only partially implemented). + +--- + +## 3. Overall Assets + +These cross-cutting assets apply across most sections; individual sections only +list assets unique to that section. + +1. **Integrity of public-API behavior** — functions return what callers expect + and don't introduce protocol corruption (request smuggling, response + splitting, framing desync). +2. **Confidentiality of data in transit** — TLS handling, header values, + cookies, request/response bodies are not leaked between connections, + sessions, or to log sinks. +3. **Availability of host application** — aiohttp does not crash, deadlock, or + exhaust CPU/memory/FDs in the host process under hostile or malformed input. +4. **Security of host application** — aiohttp does not become a vector for + attacks on the embedding application (SSRF, file disclosure, code execution, + privilege escalation through deserialisation, etc.). +5. **Reputation & supply-chain integrity** — the released artifacts on PyPI are + what maintainers built and signed; the source on GitHub matches the artifacts; + the vendored llhttp matches upstream; CI/CD secrets are not exposed. + +--- + +## 4. High-Level System Diagram + +```mermaid +flowchart LR + Untrusted([Untrusted Internet]) + Caller([Caller / host application]) + Upstream([External HTTP servers]) + + subgraph Server[Server side] + direction TB + SP[web_protocol
connection lifecycle] + PARS[HTTP parser
_http_parser.pyx + llhttp] + REQ[web_request.Request] + DISP[web_urldispatcher + middleware] + HND{user handler} + RESP[web_response.Response
FileResponse / WebSocketResponse] + WR[http_writer] + SP --> PARS --> REQ --> DISP --> HND --> RESP --> WR + end + + subgraph Client[Client side] + direction TB + CS[ClientSession] + CONN[TCPConnector
+ TLS, proxy, pooling] + RES[resolver] + CP[client_proto] + CR[client_reqrep.ClientResponse] + CS --> CONN --> RES + CONN --> CP --> CR --> CS + end + + subgraph Shared[Shared wire-protocol code] + direction TB + PARS + WR + WS[http_websocket + _websocket/] + MP[multipart] + COMP[compression_utils] + PARS -.-> WS + WR -.-> WS + end + + Untrusted -- HTTP/1, WS --> SP + WR -- HTTP/1, WS --> Untrusted + + Caller --> CS + CS --> Caller + CONN -- HTTP/1, WS --> Upstream + Upstream --> CP + + CJ[(CookieJar)] -. client only .-> CS + TR[TraceConfig] -. signals .-> CS +``` + +--- + +## 5. Scope + +The threat surface is broken down into 19 sections. Each is modeled in its own +subsection below. + +1. [HTTP/1 parser](#51-http1-parser) +2. [HTTP/1 writer](#52-http1-writer) +3. [WebSocket framing & per-message deflate](#53-websocket-framing--per-message-deflate) +4. [Multipart parsing & encoding](#54-multipart-parsing--encoding) +5. [Compression codecs](#55-compression-codecs) +6. [Streams & payloads](#56-streams--payloads) +7. [Server connection lifecycle](#57-server-connection-lifecycle) +8. [Server routing & middleware](#58-server-routing--middleware) +9. [Server request/response objects](#59-server-requestresponse-objects) +10. [Server static file serving](#510-server-static-file-serving) +11. [Server-side WebSocket handler](#511-server-side-websocket-handler) +12. [Client API & request lifecycle](#512-client-api--request-lifecycle) +13. [Connector / TLS / proxy / pooling](#513-connector--tls--proxy--pooling) +14. [Client-side WebSocket](#514-client-side-websocket) +15. [Client auth middlewares](#515-client-auth-middlewares) +16. [Cookie handling](#516-cookie-handling) +17. [DNS resolution](#517-dns-resolution) +18. [Tracing & URL/header helpers](#518-tracing--urlheader-helpers) +19. [Build & release supply chain](#519-build--release-supply-chain) + +--- + +### 5.1. HTTP/1 parser + +**Scope.** Parsing of HTTP/1.0 and HTTP/1.1 request and response messages — +request/status line, header block, chunked transfer-encoding, content-length +framing, trailers — and the surface where parsed values flow into the rest of +the library. Out of scope here: WebSocket framing ([§5.3](#53-websocket-framing--per-message-deflate)), multipart bodies +([§5.4](#54-multipart-parsing--encoding)), compression ([§5.5](#55-compression-codecs)), HTTP-writer-side framing ([§5.2](#52-http1-writer)). + +**Components covered.** + +- `aiohttp/_http_parser.pyx` — Cython wrapper over vendored llhttp, default in + CPython builds. +- `aiohttp/_cparser.pxd` — Cython declarations for llhttp. +- `aiohttp/http_parser.py` — pure-Python `HttpRequestParser` / `HttpResponseParser` + used as a fallback (and as the canonical implementation when + `AIOHTTP_NO_EXTENSIONS=1`). +- `aiohttp/_find_header.pxd` / `aiohttp/_find_header.h` — header-name interning. +- `aiohttp/http_exceptions.py` — `BadHttpMessage`, `BadHttpMethod`, + `BadStatusLine`, `LineTooLong`, `InvalidHeader`, `TransferEncodingError`, + `ContentLengthError`. +- `vendor/llhttp/` — vendored upstream parser, version `9.3.1` (see + `vendor/llhttp/package.json`). Generated via `make generate-llhttp`. + +**Selection.** A conditional re-import at the bottom of +`aiohttp/http_parser.py` re-binds the public names to the Cython parser when +`_http_parser` imports successfully and `AIOHTTP_NO_EXTENSIONS` is unset. There is no hybrid mode — both request and +response parsers come from the same backend, so an inconsistent +request-Cython/response-pure-Python configuration cannot occur in supported +builds. + +**Trust boundaries & data flow.** + +```mermaid +flowchart LR + Wire([Untrusted bytes]) --> Feed[parser.feed_data] + Feed --> Llhttp[llhttp / Python state machine] + Llhttp -->|RawRequestMessage
RawResponseMessage| Caller[web_protocol / client_proto] + Llhttp -->|StreamReader feed| Body[(Request/response body)] + Caller --> ReqResp[Request / ClientResponse] + ReqResp --> User([User handler / caller]) +``` + +The parser is invoked on every byte that arrives from a socket, before any +authentication. **Everything fed into `feed_data` is attacker-controlled** on +the server side and **upstream-controlled** on the client side (proxies, +upstream services, malicious origins reached via client). The output +(`RawRequestMessage` / `RawResponseMessage`, raw header tuples, body chunks +into `StreamReader`) is then handed to `web_protocol.RequestHandler` and +`client_proto.ResponseHandler` respectively. + +**Trust assumptions about parser output:** + +- Header names are validated against a token regex; values are not normalised + beyond `lstrip`/`rstrip` and CR/LF/NUL rejection. +- Header values are decoded `utf-8` with `surrogateescape`, so non-UTF-8 bytes + are *preserved* and *can round-trip back to the wire* if downstream code + re-emits them. Any sanitisation downstream of the parser is the + responsibility of consumers (logging, header reflection, proxying). +- Methods are accepted as any RFC 7230 token; the parser does not canonicalise + case. +- Versions are accepted by the regex `HTTP/(\d)\.(\d)` — i.e. `HTTP/0.9`, + `HTTP/2.0`, etc. all parse without rejection, even though they cannot be + served correctly. + +**Assets at risk.** + +- **Framing integrity** — that one wire message corresponds to one parsed + message; nothing the parser accepts can cause a desync between aiohttp and + an upstream/downstream peer (request smuggling). +- **Allocator safety** — that a malicious peer cannot drive memory or CPU + usage to denial of service through parser-controlled allocations. +- **Bytewise transparency** — that bytes accepted by the parser cannot inject + new framing or new header semantics downstream (CRLF injection, NUL + smuggling). + +**Threats (STRIDE).** + +| # | Component / Vector | STRIDE | Threat | Risk | +| :--- | :--- | :--- | :--- | :--- | +| 1.1 | Request line / status line | T | Smuggling via duplicate / conflicting framing headers (`Content-Length` × N, `Content-Length` + `Transfer-Encoding`, obfuscated `Transfer-Encoding`). | High | +| 1.2 | Header block, line endings | T | Smuggling via bare-LF, obs-fold, optional CR-before-LF on the *request* parser. Request parser is strict; lenient flags apply only to the response parser. | Medium | +| 1.3 | Header values, CR/LF/NUL | T / I | CRLF injection enabling response splitting / header injection if downstream re-emits values verbatim. Historically [CVE-2023-37276](https://github.com/aio-libs/aiohttp/security/advisories/GHSA-45c4-8wx5-qw6w). | High | +| 1.4 | Header values, surrogateescape decode | I / T | Non-UTF-8 bytes round-trip through `Headers` and may be reflected by user code / proxies / logs into untrusted contexts. | Medium | +| 1.5 | HTTP version regex | T | `HTTP/0.9` and `HTTP/2.0` accepted on the wire, opening a small surface for protocol-confusion against intermediaries that handle these specially. | Low | +| 1.6 | Method token | I / T | Methods are not case-canonicalised; arbitrary tokens up to `max_line_size` accepted. May confuse downstream method-based authorisation if user code compares case-sensitively. | Low | +| 1.7 | `Content-Length` parsing | T | Negative or non-decimal CL handling, multiple comma-separated CLs, CL with leading `+`/whitespace. | Medium | +| 1.8 | `Transfer-Encoding: chunked` parsing | T | Lenient acceptance (`xchunked`, `chunked, identity`, doubled `chunked`) leading to smuggling against a non-aiohttp peer that interprets differently. | Medium | +| 1.9 | Chunk size parsing | D | No upper bound on chunk-size value (Python unbounded int); huge chunk size could drive allocator before `client_max_size` rejects body. Mitigated by [§5.7](#57-server-connection-lifecycle) / `client_max_size`. | Low–Med | +| 1.10 | Chunk extensions | D / T | Unbounded chunk-extension consumption per chunk; weak validation of extension syntax. | Low | +| 1.11 | Parser error reporting | I | Exception messages may include up to ~100 bytes of malformed input, which can be surfaced in 4xx error bodies, logs, or `DEBUG=True` traces. | Low | +| 1.12 | Cython ⇄ pure-Python divergence | T / S | Behaviour differences between llhttp and the Python fallback may produce parser-confusion if a deployment unintentionally switches backends (e.g. a user installs without compiled extensions). | Med | +| 1.13 | Vendored llhttp version drift | S / T | An upstream llhttp CVE not picked up by aiohttp's vendoring cadence remains exploitable until `make generate-llhttp` is re-run and released. | Medium | +| 1.14 | Build/regen of llhttp (`make generate-llhttp`) | S / T | Local tampering or supply-chain compromise of the npm `llhttp` package gets baked into the vendored C. Covered in [§5.19](#519-build--release-supply-chain) but originates here. | Medium | + +**Mitigations.** + +| # | Threat | Existing | Recommended | +| :--- | :--- | :--- | :--- | +| 1.1 | Smuggling via duplicate framing headers | llhttp rejects conflicting `Content-Length`. `http_parser.py:HttpRequestParserPy.parse_headers` rejects coexistence of CL + `Transfer-Encoding: chunked`. The full `SINGLETON_HEADERS` set (CL, CT, Host, TE, ETag, etc.) is duplicate-rejected by the request parser (strict mode); `#12302` disabled this check on the response parser (lax mode), since real-world servers commonly send duplicate `Content-Type` / `Server`. | If new singleton-sensitive headers emerge in HTTP/1.1 RFC errata, add to `SINGLETON_HEADERS`. | +| 1.2 | Lenient response parsing | Lenient flags (`llhttp_set_lenient_headers`, `llhttp_set_lenient_optional_cr_before_lf`, `llhttp_set_lenient_spaces_after_chunk_size`) are only enabled on the **response** parser and only when `DEBUG` is False (set in `HttpResponseParser.__init__`). The **request** parser is strict. | Documented design decision: keep lenient response parsing for real-world server interop | +| 1.3 | CRLF / NUL in header values | Bytes `\r`, `\n`, `\x00` rejected in header values (`_http_parser.pyx` callbacks; `http_parser.py:HeadersParser.parse_headers`). | Keep regression tests in `tests/test_http_parser.py` covering each forbidden byte both in name and value, and across both Cython and pure-Python parsers. | +| 1.4 | Non-UTF-8 round-trip | None at parser layer (intentional — preserving original bytes is required for some use cases). | **Document in user-facing docs that header values are bytes-preserving; warn against reflecting headers verbatim into responses, logs, or sub-requests without re-validation.** | +| 1.5 | HTTP version regex accepts 0.9 / 2.0 | None (regex is permissive). | **Tighten `VERSRE` (and llhttp configuration if possible) to reject anything outside `HTTP/1.0` and `HTTP/1.1`.** | +| 1.6 | Method-case round-trip | Method token validated by regex; not canonicalised. | **Document that user route handlers / authorization checks should compare methods case-sensitively to the canonical RFC tokens, or use the framework's `web.RouteTableDef` decorators which already match canonical methods.** | +| 1.7 | `Content-Length` parsing | llhttp validates CL is decimal and non-negative; pure-Python parser validates via `DIGITS.fullmatch(r"\d+")` before `int(...)`, rejecting `+`/`-`/non-ASCII-digit forms (`test_bad_headers`, `test_headers_content_length_err_*` cover these). | None. Cross-backend parity is covered by the shared parser tests. | +| 1.8 | `Transfer-Encoding` lenience | `_is_chunked_te` requires `chunked` to be the last value; duplicate `chunked` rejected (`#10611`). Request parser strict. | None. | +| 1.9 | Chunk-size DoS | The parser doesn't cap chunk size, but **server-side body length is bounded by `client_max_size` (default `1 MiB`)** in `web_request.py:BaseRequest.read`. Client-side responses are bounded by user-supplied `max_body_size` / streaming reads. | None. If a cap is ever needed at the parser level, plumb it through `HttpPayloadParser`. | +| 1.10 | Chunk-extension DoS | Chunk-extension content is bounded by the same wire-level size constraints (it shares the chunk-size line with `max_line_size`). | **Add an explicit test that chunk-extension flooding cannot blow past `max_line_size`.** | +| 1.11 | Parser error reflection | `http_parser.py` truncates to `[:100]` for line errors. Server-side error path renders 4xx with the exception message; tracebacks only when `DEBUG=True`. | **Audit any path where `BadHttpMessage` content is reflected to the client unsanitised (especially in custom `web_log` configurations).** | +| 1.12 | Cython ⇄ pure-Python divergence | `tests/test_http_parser.py` parameterises tests over `REQUEST_PARSERS` / `RESPONSE_PARSERS` (pure-Python always; Cython when the extension imports). The high-leverage attack vectors are already covered under both backends: CL+TE (`test_content_length_transfer_encoding`), CL×N (`test_duplicate_singleton_header_rejected`), obs-fold (`test_reject_obsolete_line_folding`, `test_http_response_parser_obs_line_folding*`), CR/LF/NUL (`test_bad_headers`, `test_http_response_parser_null_byte_in_header_value`, `test_http_response_parser_bad_crlf`), version regex (`test_http_request_parser_bad_version*`, `test_http_response_parser_bad_version*`). | None. When new attack vectors emerge, add them to the parameterised tests. | +| 1.13 | llhttp version drift | Manual upgrade via `make generate-llhttp`; vendor pinned in `vendor/llhttp/package.json`. | Track upstream releases (e.g. via Dependabot rule for `vendor/llhttp/package.json`), bump on every llhttp release, regenerate in CI. | +| 1.14 | npm-side compromise of `llhttp` | The vendored output is checked into git, so a compromise during a future regen would be detectable in PR review. See [§5.19](#519-build--release-supply-chain). | **Make the llhttp build reproducible: pin Node.js version, commit the npm lockfile, and on every bump verify the regenerated C against upstream's release tarballs before committing.** | + +**Past advisories / hardening (recap).** + +- **GHSA-xx9p-xxvh-7g8j (CVE-2023-47641)** (3.8.0) — CL-vs-TE divergence + between the Cython and pure-Python parsers, allowing request smuggling + against deployments that switched backends. +- **CVE-2023-37276 / GHSA-45c4-8wx5-qw6w** (3.8.5) — HTTP request smuggling + via CR/LF/NUL in header values. Both parsers reject these bytes at the + byte level. +- **GHSA-pjjw-qhg8-p2p9** (3.8.6) — smuggling pair in vendored llhttp 8.1.1; + fixed by bumping llhttp to 9. +- **GHSA-gfw2-4jvh-wgfg / GHSA-8qpw-xqxj-h4r2** (3.8.6 / 3.9.2) — pure-Python + parser accepted lenient separators / weak RFC validation that llhttp + rejected. +- **GHSA-8495-4g3g-x7pr (CVE-2024-52304)** (3.10.11) — chunk-extension + newline smuggling in the pure-Python parser. +- **GHSA-9548-qrrj-x5pj (CVE-2025-53643)** (3.12.14) — request smuggling + via the chunked-trailer section in the pure-Python parser. +- **GHSA-69f9-5gxw-wvc2 (CVE-2025-69224)** (3.13.3) — Unicode codepoints + matched by `\d` in the pure-Python parser's regexes were treated as + digits. +- **GHSA-g84x-mcqj-x9qq** (3.13.3) — CPU-DoS on `request.read()` when + the body arrives as a very large number of small chunks. +- **PR #12137** (3.13.4) — precautionary hardening: pure-Python parser + now explicitly rejects duplicate `Transfer-Encoding: chunked` on + the request parser. +- **GHSA-c427-h43c-vf67** (3.13.4) — duplicate `Host` header accepted + in request parser, bypassing `Application.add_domain()` host-based + routing / authorisation. Fixed by adding `Host` to the strict + request-parser singleton rejection set. +- **GHSA-63hf-3vf5-4wqf (CVE-2026-34520)** (3.13.4) — llhttp accepted + NUL / control bytes in *response* header values, leaving the response + parser weaker than the request parser. Fixed by tightening the + response-side byte check. +- **GHSA-w2fm-2cpv-w7v5 (CVE-2026-22815)** (3.13.4) — uncapped memory + growth on long header / trailer blocks. Fixed by enforcing + `max_field_size` / `max_headers` on the trailer block too. +- **PR #12302** (3.13.5) — duplicate-singleton-header rejection + was breaking real-world response parsing (servers like Google APIs / + Werkzeug emit duplicate `Content-Type` / `Server`); fix disables the + check on the response parser (lax mode) while keeping it on the + request parser (strict). + +These are all currently in place; this section assumes no regression. + +--- From 75dd112eab47f70baf232b33cc19c4addb24f994 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 May 2026 09:09:43 -0700 Subject: [PATCH 098/191] [PR #12581/77e2e2d7 backport][3.13] Shrink slow client and URL dispatcher benchmarks (#12586) closed #12570. Fixes #12569 --- CHANGES/12569.misc.rst | 3 +++ tests/test_benchmarks_web_urldispatcher.py | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 CHANGES/12569.misc.rst diff --git a/CHANGES/12569.misc.rst b/CHANGES/12569.misc.rst new file mode 100644 index 00000000000..32af3f259b4 --- /dev/null +++ b/CHANGES/12569.misc.rst @@ -0,0 +1,3 @@ +Reduced payload sizes and request counts in the slowest client and URL +dispatcher benchmarks so they no longer dominate CI runtime +-- by :user:`bdraco`. diff --git a/tests/test_benchmarks_web_urldispatcher.py b/tests/test_benchmarks_web_urldispatcher.py index 936ed6320ed..c350af871b7 100644 --- a/tests/test_benchmarks_web_urldispatcher.py +++ b/tests/test_benchmarks_web_urldispatcher.py @@ -359,7 +359,7 @@ async def handler(request: web.Request) -> NoReturn: requests = [ _mock_request(method="GET", path=f"/api/{customer}/update") - for customer in range(250) + for customer in range(150) ] async def run_url_dispatcher_benchmark() -> Optional[web.UrlMappingMatchInfo]: @@ -401,7 +401,7 @@ async def handler(request: web.Request) -> NoReturn: alnums = string.ascii_letters + string.digits requests = [] - for i in range(250): + for i in range(150): owner = "".join(random.sample(alnums, 10)) repo = "".join(random.sample(alnums, 10)) pull_number = random.randint(0, 250) @@ -472,7 +472,7 @@ async def handler(request: web.Request) -> NoReturn: alnums = string.ascii_letters + string.digits requests = [] - for i in range(250): + for i in range(150): owner = "".join(random.sample(alnums, 10)) repo = "".join(random.sample(alnums, 10)) pull_number = random.randint(0, 250) From e936eee9e1a4082853414d8a77c0c0e8893e9896 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 May 2026 09:17:39 -0700 Subject: [PATCH 099/191] [PR #12581/77e2e2d7 backport][3.14] Shrink slow client and URL dispatcher benchmarks (#12587) closed #12570. Fixes #12569 --- CHANGES/12569.misc.rst | 3 +++ tests/test_benchmarks_client.py | 20 ++++++++++---------- tests/test_benchmarks_web_urldispatcher.py | 6 +++--- 3 files changed, 16 insertions(+), 13 deletions(-) create mode 100644 CHANGES/12569.misc.rst diff --git a/CHANGES/12569.misc.rst b/CHANGES/12569.misc.rst new file mode 100644 index 00000000000..32af3f259b4 --- /dev/null +++ b/CHANGES/12569.misc.rst @@ -0,0 +1,3 @@ +Reduced payload sizes and request counts in the slowest client and URL +dispatcher benchmarks so they no longer dominate CI runtime +-- by :user:`bdraco`. diff --git a/tests/test_benchmarks_client.py b/tests/test_benchmarks_client.py index 3c362a500a7..8ebedcc4776 100644 --- a/tests/test_benchmarks_client.py +++ b/tests/test_benchmarks_client.py @@ -177,14 +177,14 @@ def _run() -> None: loop.run_until_complete(run_client_benchmark()) -def test_one_hundred_get_requests_with_10mb_chunked_payload( +def test_one_hundred_get_requests_with_1mb_chunked_payload( loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient, benchmark: BenchmarkFixture, ) -> None: - """Benchmark 100 GET requests with a payload of 10 MiB using read.""" + """Benchmark 100 GET requests with a 1 MiB chunked payload using read.""" message_count = 100 - payload = b"a" * (10 * 2**20) + payload = b"a" * 2**20 async def handler(request: web.Request) -> web.Response: resp = web.Response(body=payload) @@ -206,14 +206,14 @@ def _run() -> None: loop.run_until_complete(run_client_benchmark()) -def test_one_hundred_get_requests_iter_chunks_on_10mb_chunked_payload( +def test_one_hundred_get_requests_iter_chunks_on_1mb_chunked_payload( loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient, benchmark: BenchmarkFixture, ) -> None: - """Benchmark 100 GET requests with a payload of 10 MiB using iter_chunks.""" + """Benchmark 100 GET requests with a 1 MiB chunked payload using iter_chunks.""" message_count = 100 - payload = b"a" * (10 * 2**20) + payload = b"a" * 2**20 async def handler(request: web.Request) -> web.Response: resp = web.Response(body=payload) @@ -327,14 +327,14 @@ def _run() -> None: loop.run_until_complete(run_client_benchmark()) -def test_one_hundred_get_requests_with_10mb_content_length_payload( +def test_one_hundred_get_requests_with_1mb_content_length_payload( loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient, benchmark: BenchmarkFixture, ) -> None: - """Benchmark 100 GET requests with a payload of 10 MiB.""" + """Benchmark 100 GET requests with a content-length payload of 1 MiB.""" message_count = 100 - payload = b"a" * (10 * 2**20) + payload = b"a" * 2**20 headers = {hdrs.CONTENT_LENGTH: str(len(payload))} async def handler(request: web.Request) -> web.Response: @@ -479,7 +479,7 @@ def test_ten_streamed_responses_iter_chunked_1mb( """Benchmark 10 streamed responses using iter_chunked 1 MiB.""" message_count = 10 MB = 2**20 - data = b"x" * 10 * MB + data = b"x" * 6 * MB async def handler(request: web.Request) -> web.StreamResponse: resp = web.StreamResponse() diff --git a/tests/test_benchmarks_web_urldispatcher.py b/tests/test_benchmarks_web_urldispatcher.py index 339eaef8a0e..f01adb6da5c 100644 --- a/tests/test_benchmarks_web_urldispatcher.py +++ b/tests/test_benchmarks_web_urldispatcher.py @@ -359,7 +359,7 @@ async def handler(request: web.Request) -> NoReturn: requests = [ _mock_request(method="GET", path=f"/api/{customer}/update") - for customer in range(250) + for customer in range(150) ] async def run_url_dispatcher_benchmark() -> web.UrlMappingMatchInfo | None: @@ -401,7 +401,7 @@ async def handler(request: web.Request) -> NoReturn: alnums = string.ascii_letters + string.digits requests = [] - for i in range(250): + for i in range(150): owner = "".join(random.sample(alnums, 10)) repo = "".join(random.sample(alnums, 10)) pull_number = random.randint(0, 250) @@ -472,7 +472,7 @@ async def handler(request: web.Request) -> NoReturn: alnums = string.ascii_letters + string.digits requests = [] - for i in range(250): + for i in range(150): owner = "".join(random.sample(alnums, 10)) repo = "".join(random.sample(alnums, 10)) pull_number = random.randint(0, 250) From f36abe1138733ff5a818eff56dffdfaff51179aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 16:57:52 +0000 Subject: [PATCH 100/191] Bump ast-serialize from 0.3.0 to 0.4.0 (#12543) Bumps [ast-serialize](https://github.com/mypyc/ast_serialize) from 0.3.0 to 0.4.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 e2e39becce8..74128b5c99d 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -18,7 +18,7 @@ alabaster==1.0.0 # via sphinx annotated-types==0.7.0 # via pydantic -ast-serialize==0.3.0 +ast-serialize==0.4.0 # via mypy async-timeout==5.0.1 ; python_version < "3.11" # via diff --git a/requirements/dev.txt b/requirements/dev.txt index 236052a9f8d..0732c1da1e3 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -18,7 +18,7 @@ alabaster==1.0.0 # via sphinx annotated-types==0.7.0 # via pydantic -ast-serialize==0.3.0 +ast-serialize==0.4.0 # via mypy async-timeout==5.0.1 ; python_version < "3.11" # via diff --git a/requirements/lint.txt b/requirements/lint.txt index cc38079bb98..138d631a33e 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -8,7 +8,7 @@ aiodns==4.0.0 # via -r requirements/lint.in annotated-types==0.7.0 # via pydantic -ast-serialize==0.3.0 +ast-serialize==0.4.0 # via mypy async-timeout==5.0.1 # via valkey diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 48a9114ab27..bea1cee2d18 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -6,7 +6,7 @@ # annotated-types==0.7.0 # via pydantic -ast-serialize==0.3.0 +ast-serialize==0.4.0 # via mypy blockbuster==1.5.26 # via -r requirements/test-common.in diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 2013c7512c7..20d1e4288f3 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -12,7 +12,7 @@ aiosignal==1.4.0 # via -r requirements/runtime-deps.in annotated-types==0.7.0 # via pydantic -ast-serialize==0.3.0 +ast-serialize==0.4.0 # via mypy async-timeout==5.0.1 ; python_version < "3.11" # via -r requirements/runtime-deps.in diff --git a/requirements/test.txt b/requirements/test.txt index c3dcb79a2b1..11d061de5b8 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -12,7 +12,7 @@ aiosignal==1.4.0 # via -r requirements/runtime-deps.in annotated-types==0.7.0 # via pydantic -ast-serialize==0.3.0 +ast-serialize==0.4.0 # via mypy async-timeout==5.0.1 ; python_version < "3.11" # via -r requirements/runtime-deps.in From c7562a6195e403e0c01ba100a4fbff04f1bb35c2 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 19:31:12 +0000 Subject: [PATCH 101/191] [PR #12589/b7e19ccd backport][3.13] Gate isal test dependency to CPython (#12590) Co-authored-by: J. Nick Koston --- CHANGES/12589.contrib.rst | 7 +++++++ requirements/constraints.txt | 2 +- requirements/dev.txt | 2 +- requirements/test-common.in | 2 +- requirements/test-common.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 7 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 CHANGES/12589.contrib.rst diff --git a/CHANGES/12589.contrib.rst b/CHANGES/12589.contrib.rst new file mode 100644 index 00000000000..dc7f6400d57 --- /dev/null +++ b/CHANGES/12589.contrib.rst @@ -0,0 +1,7 @@ +Restricted the ``isal`` test dependency to CPython, since +``isal`` 1.8.0 stopped publishing PyPy wheels and the source +build requires ``nasm``, which is not available on the CI +runners. The ``parametrize_zlib_backend`` fixture already +calls ``pytest.importorskip``, so PyPy continues to exercise +the ``zlib`` and ``zlib_ng`` backends with no further +changes -- by :user:`bdraco`. diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 495ab431cc8..131b0c4f0fe 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -99,7 +99,7 @@ imagesize==1.4.1 # via sphinx iniconfig==2.1.0 # via pytest -isal==1.7.2 ; python_version < "3.14" +isal==1.7.2 ; python_version < "3.14" and implementation_name == "cpython" # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/dev.txt b/requirements/dev.txt index b3f9fc2981b..2b10c553956 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -97,7 +97,7 @@ imagesize==1.4.1 # via sphinx iniconfig==2.1.0 # via pytest -isal==1.7.2 ; python_version < "3.14" +isal==1.7.2 ; python_version < "3.14" and implementation_name == "cpython" # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/test-common.in b/requirements/test-common.in index 84193d7bc11..b2f6dd869e1 100644 --- a/requirements/test-common.in +++ b/requirements/test-common.in @@ -1,7 +1,7 @@ blockbuster coverage freezegun -isal; python_version < "3.14" # no wheel for 3.14 +isal; python_version < "3.14" and implementation_name == "cpython" # no wheel for 3.14, no PyPy wheel for 1.8.0+ mypy; implementation_name == "cpython" pkgconfig proxy.py >= 2.4.4rc5 diff --git a/requirements/test-common.txt b/requirements/test-common.txt index be1987311af..16c3a976898 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -32,7 +32,7 @@ idna==3.11 # via trustme iniconfig==2.1.0 # via pytest -isal==1.8.0 ; python_version < "3.14" +isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython" # via -r requirements/test-common.in markdown-it-py==4.0.0 # via rich diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 0d9df55b375..18abb27d9cf 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -53,7 +53,7 @@ idna==3.11 # yarl iniconfig==2.1.0 # via pytest -isal==1.8.0 ; python_version < "3.14" +isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython" # via -r requirements/test-common.in markdown-it-py==4.0.0 # via rich diff --git a/requirements/test.txt b/requirements/test.txt index 95b278f12fb..52b74930a05 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -53,7 +53,7 @@ idna==3.11 # yarl iniconfig==2.1.0 # via pytest -isal==1.7.2 ; python_version < "3.14" +isal==1.7.2 ; python_version < "3.14" and implementation_name == "cpython" # via -r requirements/test-common.in markdown-it-py==3.0.0 # via rich From f6f903219ab9634362afcaae029393a2b6c29335 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 20:48:09 +0000 Subject: [PATCH 102/191] [PR #12592/1a7de6fa backport][3.13] Fix flakey test_tcp_connector_fingerprint_ok by aborting SSL shutdown (#12593) Co-authored-by: J. Nick Koston --- CHANGES/12592.contrib.rst | 6 ++++++ tests/test_client_functional.py | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 CHANGES/12592.contrib.rst diff --git a/CHANGES/12592.contrib.rst b/CHANGES/12592.contrib.rst new file mode 100644 index 00000000000..76d2f8b7035 --- /dev/null +++ b/CHANGES/12592.contrib.rst @@ -0,0 +1,6 @@ +Fixed a flakey ``test_tcp_connector_fingerprint_ok`` by aborting +the SSL shutdown on the test's TCP connector before returning. +The graceful TLS close was occasionally outliving the test event +loop on one of the CI jobs, and the teardown ``gc.collect()`` +then surfaced the still-open transport as a +``PytestUnraisableExceptionWarning`` -- by :user:`bdraco`. diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 118ce19ca64..dde06fdc6dd 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -831,6 +831,12 @@ async def handler(request): async with client.get("/") as resp: assert resp.status == 200 + # Abort the SSL shutdown explicitly so the pooled TLS transport is + # released before the test loop tears down. Otherwise a slow graceful + # close can outlive the loop, and the teardown gc.collect() finalises + # the still-open socket as an unraisable ResourceWarning. + await connector.close(abort_ssl=True) + async def test_tcp_connector_fingerprint_fail( aiohttp_server, From cced874ce4a554716f16672cfc66cf1cf89f989b Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 20:52:43 +0000 Subject: [PATCH 103/191] [PR #12592/1a7de6fa backport][3.14] Fix flakey test_tcp_connector_fingerprint_ok by aborting SSL shutdown (#12594) Co-authored-by: J. Nick Koston --- CHANGES/12592.contrib.rst | 6 ++++++ tests/test_client_functional.py | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 CHANGES/12592.contrib.rst diff --git a/CHANGES/12592.contrib.rst b/CHANGES/12592.contrib.rst new file mode 100644 index 00000000000..76d2f8b7035 --- /dev/null +++ b/CHANGES/12592.contrib.rst @@ -0,0 +1,6 @@ +Fixed a flakey ``test_tcp_connector_fingerprint_ok`` by aborting +the SSL shutdown on the test's TCP connector before returning. +The graceful TLS close was occasionally outliving the test event +loop on one of the CI jobs, and the teardown ``gc.collect()`` +then surfaced the still-open transport as a +``PytestUnraisableExceptionWarning`` -- by :user:`bdraco`. diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 6d17172457b..4b4d12443b6 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -822,6 +822,12 @@ async def handler(request): async with client.get("/") as resp: assert resp.status == 200 + # Abort the SSL shutdown explicitly so the pooled TLS transport is + # released before the test loop tears down. Otherwise a slow graceful + # close can outlive the loop, and the teardown gc.collect() finalises + # the still-open socket as an unraisable ResourceWarning. + await connector.close(abort_ssl=True) + async def test_tcp_connector_fingerprint_fail( aiohttp_server, From 696761d80cbaf104dc0d7a49b6950e0a88ef6e6c Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 21:38:51 +0000 Subject: [PATCH 104/191] [PR #12589/b7e19ccd backport][3.14] Gate isal test dependency to CPython (#12591) Co-authored-by: J. Nick Koston --- CHANGES/12589.contrib.rst | 7 +++++++ requirements/constraints.txt | 2 +- requirements/dev.txt | 2 +- requirements/test-common.in | 2 +- requirements/test-common.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 7 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 CHANGES/12589.contrib.rst diff --git a/CHANGES/12589.contrib.rst b/CHANGES/12589.contrib.rst new file mode 100644 index 00000000000..dc7f6400d57 --- /dev/null +++ b/CHANGES/12589.contrib.rst @@ -0,0 +1,7 @@ +Restricted the ``isal`` test dependency to CPython, since +``isal`` 1.8.0 stopped publishing PyPy wheels and the source +build requires ``nasm``, which is not available on the CI +runners. The ``parametrize_zlib_backend`` fixture already +calls ``pytest.importorskip``, so PyPy continues to exercise +the ``zlib`` and ``zlib_ng`` backends with no further +changes -- by :user:`bdraco`. diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 74128b5c99d..d9a72a96a16 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -99,7 +99,7 @@ imagesize==2.0.0 # via sphinx iniconfig==2.3.0 # via pytest -isal==1.7.2 ; python_version < "3.14" +isal==1.7.2 ; python_version < "3.14" and implementation_name == "cpython" # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 0732c1da1e3..75d3eaaa4ab 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -97,7 +97,7 @@ imagesize==2.0.0 # via sphinx iniconfig==2.3.0 # via pytest -isal==1.7.2 ; python_version < "3.14" +isal==1.7.2 ; python_version < "3.14" and implementation_name == "cpython" # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/test-common.in b/requirements/test-common.in index 84193d7bc11..b2f6dd869e1 100644 --- a/requirements/test-common.in +++ b/requirements/test-common.in @@ -1,7 +1,7 @@ blockbuster coverage freezegun -isal; python_version < "3.14" # no wheel for 3.14 +isal; python_version < "3.14" and implementation_name == "cpython" # no wheel for 3.14, no PyPy wheel for 1.8.0+ mypy; implementation_name == "cpython" pkgconfig proxy.py >= 2.4.4rc5 diff --git a/requirements/test-common.txt b/requirements/test-common.txt index bea1cee2d18..f0234b57ba7 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -32,7 +32,7 @@ idna==3.15 # via trustme iniconfig==2.3.0 # via pytest -isal==1.8.0 ; python_version < "3.14" +isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython" # via -r requirements/test-common.in librt==0.11.0 # via mypy diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 20d1e4288f3..baf5c381d3f 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -56,7 +56,7 @@ idna==3.15 # yarl iniconfig==2.3.0 # via pytest -isal==1.8.0 ; python_version < "3.14" +isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython" # via -r requirements/test-common.in librt==0.11.0 # via mypy diff --git a/requirements/test.txt b/requirements/test.txt index 11d061de5b8..851bc7ab40b 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -56,7 +56,7 @@ idna==3.15 # yarl iniconfig==2.3.0 # via pytest -isal==1.7.2 ; python_version < "3.14" +isal==1.7.2 ; python_version < "3.14" and implementation_name == "cpython" # via -r requirements/test-common.in librt==0.11.0 # via mypy From 5a026a28e51aa2ff236131f2eb0e0161f67c49ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 22:40:39 +0000 Subject: [PATCH 105/191] Bump isal from 1.7.2 to 1.8.0 (#11589) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [isal](https://github.com/pycompression/python-isal) from 1.7.2 to 1.8.0.
Release notes

Sourced from isal's releases.

version 1.8.0

  • Python 3.14 is supported.
  • Python 3.8 and 3.9 are no longer supported.
  • Fix an issue where flushing using igzip_threaded caused a gzip end of stream and started a new gzip stream. In essence creating a concatenated gzip stream. Now it is in concordance with how single threaded gzip streams are flushed using Z_SYNC_FLUSH.
  • Change build backend to setuptools-scm which is more commonly used and supported.
  • Include test packages in the source distribution, so source distribution installations can be verified.
  • Fix an issue where some tests failed because they ignored PYTHONPATH.
  • Enable support for free-threading and build free-threaded wheels for CPython 3.14. Thanks to @​lysnikolaou and @​ngoldbaum.
Changelog

Sourced from isal's changelog.

version 1.8.0

  • Python 3.14 is supported.
  • Python 3.8 and 3.9 are no longer supported.
  • Fix an issue where flushing using igzip_threaded caused a gzip end of stream and started a new gzip stream. In essence creating a concatenated gzip stream. Now it is in concordance with how single threaded gzip streams are flushed using Z_SYNC_FLUSH.
  • Change build backend to setuptools-scm which is more commonly used and supported.
  • Include test packages in the source distribution, so source distribution installations can be verified.
  • Fix an issue where some tests failed because they ignored PYTHONPATH.
  • Enable support for free-threading and build free-threaded wheels for CPython 3.14. Thanks to @​lysnikolaou and @​ngoldbaum.
Commits
  • bcaaa9b Use a valid documentation URL
  • 1eeaf31 Merge pull request #244 from pycompression/release_1.8.0
  • ddcc966 Prepare release 1.8.0
  • bb40ffa Merge pull request #233 from lysnikolaou/free-threading
  • 8e9681d Credit where it's due
  • 9492937 Update test_freethreading.py
  • 0b9574a Fix linting issues
  • 474783e Only support 3.14 for threaded builds.
  • edf5371 Merge branch 'develop' into free-threading
  • 7d204aa Merge pull request #242 from pycompression/314
  • Additional commits viewable in compare view

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 d9a72a96a16..95ea5248658 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -99,7 +99,7 @@ imagesize==2.0.0 # via sphinx iniconfig==2.3.0 # via pytest -isal==1.7.2 ; python_version < "3.14" and implementation_name == "cpython" +isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython" # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 75d3eaaa4ab..eabac796cb9 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -97,7 +97,7 @@ imagesize==2.0.0 # via sphinx iniconfig==2.3.0 # via pytest -isal==1.7.2 ; python_version < "3.14" and implementation_name == "cpython" +isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython" # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/lint.txt b/requirements/lint.txt index 138d631a33e..ef45375b634 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -44,7 +44,7 @@ idna==3.15 # via trustme iniconfig==2.3.0 # via pytest -isal==1.7.2 +isal==1.8.0 # via -r requirements/lint.in librt==0.11.0 # via mypy diff --git a/requirements/test.txt b/requirements/test.txt index 851bc7ab40b..4ea290d8e2c 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -56,7 +56,7 @@ idna==3.15 # yarl iniconfig==2.3.0 # via pytest -isal==1.7.2 ; python_version < "3.14" and implementation_name == "cpython" +isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython" # via -r requirements/test-common.in librt==0.11.0 # via mypy From 5cf0c708f04f194273479c2d9f77be3a9abba3a6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 May 2026 15:45:36 -0700 Subject: [PATCH 106/191] [PR #12595/fbf44b7e backport][3.13] Switch cibuildwheel to the uv build frontend (#12601) --- .github/workflows/ci-cd.yml | 8 +++++++- CHANGES/12595.contrib.rst | 5 +++++ docs/spelling_wordlist.txt | 1 + pyproject.toml | 7 +++++++ 4 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 CHANGES/12595.contrib.rst diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 7349fb3930a..0ae7a7eb1ed 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -459,7 +459,13 @@ jobs: run: | make cythonize - name: Build wheels - uses: pypa/cibuildwheel@v3.2.0 + uses: pypa/cibuildwheel@v3.4.1 + with: + # `build-frontend = "build[uv]"` (pyproject.toml) requires uv to be + # available on the runner for Windows and macOS. Installing + # cibuildwheel with the `uv` extra bundles uv with it; Linux + # already has uv inside the manylinux/musllinux container. + extras: uv env: CIBW_SKIP: pp* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }} CIBW_ARCHS_MACOS: x86_64 arm64 universal2 diff --git a/CHANGES/12595.contrib.rst b/CHANGES/12595.contrib.rst new file mode 100644 index 00000000000..54af2262fb1 --- /dev/null +++ b/CHANGES/12595.contrib.rst @@ -0,0 +1,5 @@ +Switched the ``cibuildwheel`` build frontend to ``build[uv]`` so +that ``uv`` provisions every build-isolation virtual environment +in the wheel matrix, replacing the per-ABI ``pip`` resolve with a +roughly sub-second ``uv`` resolve +-- by :user:`bdraco`. diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 7dc09edb127..77c421e3e79 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -1,4 +1,5 @@ abc +ABI addons aiodns aioes diff --git a/pyproject.toml b/pyproject.toml index d1550fee500..fabdf652580 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -160,6 +160,13 @@ include = [ [tool.cibuildwheel] +# `build[uv]` makes cibuildwheel use `uv` for every venv it provisions, +# both on the build side (passed to `python -m build` as `--installer=uv`) +# and when materializing the build isolation environment. uv resolves and +# installs the build requirements in roughly a second; the previous +# pip-based path took noticeably longer per ABI and ran once per matrix +# cell. +build-frontend = "build[uv]" test-command = "" # don't build PyPy wheels, install from source instead skip = "pp*" From 2382ac9449ad981b3ae7eb107c267ab8ac45bc72 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 15:45:53 -0700 Subject: [PATCH 107/191] [PR #12595/fbf44b7e backport][3.14] Switch cibuildwheel to the uv build frontend (#12598) Co-authored-by: J. Nick Koston --- .github/workflows/ci-cd.yml | 6 ++++++ CHANGES/12595.contrib.rst | 5 +++++ docs/spelling_wordlist.txt | 1 + pyproject.toml | 7 +++++++ 4 files changed, 19 insertions(+) create mode 100644 CHANGES/12595.contrib.rst diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 78953a31855..06303863c3c 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -615,6 +615,12 @@ jobs: make cythonize - name: Build wheels uses: pypa/cibuildwheel@v3.4.1 + with: + # `build-frontend = "build[uv]"` (pyproject.toml) requires uv to be + # available on the runner for Windows and macOS. Installing + # cibuildwheel with the `uv` extra bundles uv with it; Linux + # already has uv inside the manylinux/musllinux container. + extras: uv env: CIBW_SKIP: pp* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }} CIBW_ARCHS_MACOS: x86_64 arm64 universal2 diff --git a/CHANGES/12595.contrib.rst b/CHANGES/12595.contrib.rst new file mode 100644 index 00000000000..54af2262fb1 --- /dev/null +++ b/CHANGES/12595.contrib.rst @@ -0,0 +1,5 @@ +Switched the ``cibuildwheel`` build frontend to ``build[uv]`` so +that ``uv`` provisions every build-isolation virtual environment +in the wheel matrix, replacing the per-ABI ``pip`` resolve with a +roughly sub-second ``uv`` resolve +-- by :user:`bdraco`. diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index a422b523639..16f7da5ebe4 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -1,4 +1,5 @@ abc +ABI addons aiodns aioes diff --git a/pyproject.toml b/pyproject.toml index 72eaf0e04f2..c74565e4902 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -160,6 +160,13 @@ include = [ [tool.cibuildwheel] +# `build[uv]` makes cibuildwheel use `uv` for every venv it provisions, +# both on the build side (passed to `python -m build` as `--installer=uv`) +# and when materializing the build isolation environment. uv resolves and +# installs the build requirements in roughly a second; the previous +# pip-based path took noticeably longer per ABI and ran once per matrix +# cell. +build-frontend = "build[uv]" test-command = "" # don't build PyPy wheels, install from source instead skip = "pp*" From e81a7d93287a708e8759d319e84e4b3d3bf64d7b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 May 2026 17:33:00 -0700 Subject: [PATCH 108/191] [PR #12597/28d1214f backport][3.14] Stop calling socket.getfqdn() in BaseRequest.host fallback (#12605) Closes #9308. --- CHANGES/12597.breaking.rst | 1 + CHANGES/9308.breaking.rst | 12 +++++++ aiohttp/test_utils.py | 8 +++-- aiohttp/web_protocol.py | 11 ++++++ aiohttp/web_request.py | 18 ++++++++-- docs/web_reference.rst | 11 +++++- tests/test_web_request.py | 69 ++++++++++++++++++++++++++++++++------ 7 files changed, 113 insertions(+), 17 deletions(-) create mode 120000 CHANGES/12597.breaking.rst create mode 100644 CHANGES/9308.breaking.rst diff --git a/CHANGES/12597.breaking.rst b/CHANGES/12597.breaking.rst new file mode 120000 index 00000000000..651891d5065 --- /dev/null +++ b/CHANGES/12597.breaking.rst @@ -0,0 +1 @@ +9308.breaking.rst \ No newline at end of file diff --git a/CHANGES/9308.breaking.rst b/CHANGES/9308.breaking.rst new file mode 100644 index 00000000000..afb3965ab8f --- /dev/null +++ b/CHANGES/9308.breaking.rst @@ -0,0 +1,12 @@ +Stopped calling :func:`socket.getfqdn` as the fallback for +:attr:`aiohttp.web.BaseRequest.host`. :func:`socket.getfqdn` +performs blocking reverse DNS resolution on the event loop +thread and can stall a worker for many seconds when the system +resolver is slow, and could be triggered remotely by an HTTP/1.0 +request that omits the ``Host`` header. The fallback when no +``Host`` header is present is now the local socket address the +request arrived on (transport ``sockname``), or an empty string +if no transport information is available. Code that relied on +the FQDN being returned must now read it from +:func:`socket.getfqdn` directly, off the event loop +-- by :user:`bdraco`. diff --git a/aiohttp/test_utils.py b/aiohttp/test_utils.py index ccc05e1ef5d..92a49b975ec 100644 --- a/aiohttp/test_utils.py +++ b/aiohttp/test_utils.py @@ -677,11 +677,10 @@ def set_dict(app: Any, key: str, value: Any) -> None: def _create_transport(sslcontext: SSLContext | None = None) -> mock.Mock: transport = mock.Mock() - def get_extra_info(key: str) -> SSLContext | None: + def get_extra_info(key: str) -> SSLContext | tuple[str, int] | None: if key == "sslcontext": return sslcontext - else: - return None + return ("127.0.0.1", 80) if key == "sockname" else None transport.get_extra_info.side_effect = get_extra_info return transport @@ -762,6 +761,9 @@ def make_mocked_request( type(protocol).peername = mock.PropertyMock( return_value=transport.get_extra_info("peername") ) + type(protocol).sockname = mock.PropertyMock( + return_value=transport.get_extra_info("sockname") + ) type(protocol).ssl_context = mock.PropertyMock(return_value=sslcontext) if writer is sentinel: diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py index f170c8e5de6..69fe7b9bb29 100644 --- a/aiohttp/web_protocol.py +++ b/aiohttp/web_protocol.py @@ -275,6 +275,17 @@ def peername( else self.transport.get_extra_info("peername") ) + @under_cached_property + def sockname( + self, + ) -> str | tuple[str, int, int, int] | tuple[str, int] | None: + """Return sockname if available.""" + return ( + None + if self.transport is None + else self.transport.get_extra_info("sockname") + ) + @property def keepalive_timeout(self) -> float: return self._keepalive_timeout diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py index b5d5b1b6897..d3d9cc2989f 100644 --- a/aiohttp/web_request.py +++ b/aiohttp/web_request.py @@ -2,7 +2,6 @@ import datetime import io import re -import socket import string import tempfile import types @@ -190,6 +189,7 @@ def __init__( self._transport_sslcontext = protocol.ssl_context self._transport_peername = protocol.peername + self._transport_sockname = protocol.sockname if remote is not None: self._cache["remote"] = remote @@ -437,7 +437,9 @@ def host(self) -> str: - overridden value by .clone(host=new_host) call. - HOST HTTP header - - socket.getfqdn() value + - local socket address the request arrived on + (transport ``sockname``) + - empty string if no transport information is available For example, 'example.com' or 'localhost:8080'. @@ -446,7 +448,17 @@ def host(self) -> str: host = self._message.headers.get(hdrs.HOST) if host is not None: return host - return socket.getfqdn() + sockname = self._transport_sockname + if sockname is None: + return "" + if isinstance(sockname, tuple): + # AF_INET6 returns a 4-tuple (host, port, flowinfo, scopeid); + # bracket the bare address so it matches the Host-header shape + # and is a valid URL authority component. + if len(sockname) == 4: + return f"[{sockname[0]}]" + return str(sockname[0]) + return str(sockname) @reify def remote(self) -> str | None: diff --git a/docs/web_reference.rst b/docs/web_reference.rst index a3d9e1b284d..6fcb1a8f42f 100644 --- a/docs/web_reference.rst +++ b/docs/web_reference.rst @@ -118,7 +118,9 @@ and :ref:`aiohttp-web-signals` handlers. - Overridden value by :meth:`~BaseRequest.clone` call. - *Host* HTTP header - - :func:`socket.getfqdn` + - local socket address the request arrived on + (transport ``sockname``) + - empty string if no transport information is available Read-only :class:`str` property. @@ -129,6 +131,13 @@ and :ref:`aiohttp-web-signals` handlers. Call ``.clone(host=new_host)`` for setting up the value explicitly. + .. versionchanged:: 3.13 + + The fallback when no ``Host`` header is present no longer + calls :func:`socket.getfqdn`, which performed blocking + reverse-DNS resolution on the event loop. The local socket + address (transport ``sockname``) is used instead. + .. seealso:: :ref:`aiohttp-web-forwarded-support` .. attribute:: remote diff --git a/tests/test_web_request.py b/tests/test_web_request.py index fa2f257c402..d025c8c4812 100644 --- a/tests/test_web_request.py +++ b/tests/test_web_request.py @@ -1,7 +1,6 @@ import asyncio import datetime import logging -import socket import sys import time from collections.abc import Iterator, MutableMapping @@ -42,16 +41,19 @@ def test_base_ctor() -> None: URL("/path/to?a=1&b=2"), ) + protocol = mock.Mock() + protocol.ssl_context = None + protocol.peername = None + protocol.sockname = ("127.0.0.1", 80) req = BaseRequest( - message, mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock() + message, mock.Mock(), protocol, mock.Mock(), mock.Mock(), mock.Mock() ) assert "GET" == req.method assert HttpVersion(1, 1) == req.version - # MacOS may return CamelCased host name, need .lower() - # FQDN can be wider than host, e.g. - # 'fv-az397-495' in 'fv-az397-495.internal.cloudapp.net' - assert req.host.lower() in socket.getfqdn().lower() + # No Host header in this request, so host falls back to the + # local socket address the request arrived on. + assert req.host == "127.0.0.1" assert "/path/to?a=1&b=2" == req.path_qs assert "/path/to" == req.path assert "a=1&b=2" == req.query_string @@ -73,10 +75,10 @@ def test_ctor() -> None: assert "GET" == req.method assert HttpVersion(1, 1) == req.version - # MacOS may return CamelCased host name, need .lower() - # FQDN can be wider than host, e.g. - # 'fv-az397-495' in 'fv-az397-495.internal.cloudapp.net' - assert req.host.lower() in socket.getfqdn().lower() + # No Host header in this request, so host falls back to the + # local socket address the request arrived on (the default + # sockname configured by make_mocked_request). + assert req.host == "127.0.0.1" assert "/path/to?a=1&b=2" == req.path_qs assert "/path/to" == req.path assert "a=1&b=2" == req.query_string @@ -118,6 +120,53 @@ def test_deprecated_message() -> None: assert req.message == req._message +def test_host_falls_back_to_sockname_not_dns() -> None: + """Regression: request.host must not call socket.getfqdn(). + + socket.getfqdn() does blocking reverse DNS resolution on the + event loop thread and can stall a worker for many seconds when + the system resolver is slow. The fallback for a request with no + Host header is the local socket address the request arrived on, + not the system FQDN. + """ + req = make_mocked_request("GET", "/") + assert req.host == "127.0.0.1" + assert str(req.url).startswith("http://127.0.0.1") + + +def test_host_with_ipv6_sockname() -> None: + """AF_INET6 sockname is bracketed to form a valid URL authority. + + A bare IPv6 string would cause ``URL.build(authority=...)`` to + raise ``ValueError``. + """ + transport = mock.Mock() + transport.get_extra_info.side_effect = lambda key: ( + ("::1", 80, 0, 0) if key == "sockname" else None + ) + req = make_mocked_request("GET", "/", transport=transport) + assert req.host == "[::1]" + assert str(req.url) == "http://[::1]/" + + +def test_host_with_unix_socket_sockname() -> None: + """Unix-socket transports expose sockname as a str path.""" + transport = mock.Mock() + transport.get_extra_info.side_effect = lambda key: ( + "/tmp/aiohttp.sock" if key == "sockname" else None + ) + req = make_mocked_request("GET", "/", transport=transport) + assert req.host == "/tmp/aiohttp.sock" + + +def test_host_with_no_transport_sockname() -> None: + """An empty string is returned when no sockname is available.""" + transport = mock.Mock() + transport.get_extra_info.return_value = None + req = make_mocked_request("GET", "/", transport=transport) + assert req.host == "" + + def test_doubleslashes() -> None: # NB: //foo/bar is an absolute URL with foo netloc and /bar path req = make_mocked_request("GET", "/bar//foo/") From c284e2170cde9539da7796608cbbbe41f45aaa3f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 May 2026 18:11:27 -0700 Subject: [PATCH 109/191] [PR #12603/dbe83fd0 backport][3.14] Fix flaky test_handler_returns_not_response on PyPy via debug-mode fixture (#12608) --- CHANGES/12603.contrib.rst | 8 ++++++++ tests/conftest.py | 18 ++++++++++++++++++ tests/test_web_functional.py | 10 ++++++---- 3 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 CHANGES/12603.contrib.rst diff --git a/CHANGES/12603.contrib.rst b/CHANGES/12603.contrib.rst new file mode 100644 index 00000000000..51574d02868 --- /dev/null +++ b/CHANGES/12603.contrib.rst @@ -0,0 +1,8 @@ +Fixed flaky ``test_handler_returns_not_response`` and +``test_handler_returns_none`` by routing ``loop.set_debug(True)`` +through a new ``loop_debug_mode`` fixture that disables debug +mode before the ``aiohttp_client`` fixture finalizes. Leaving +debug on through teardown let PyPy 3.11's asyncio slow-callback +logger walk into ``Task.__repr__`` during connector close, +surfacing a spurious ``RuntimeWarning: coroutine was never +awaited`` -- by :user:`bdraco`. diff --git a/tests/conftest.py b/tests/conftest.py index 71e773ddca8..a1cd6a17a56 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -165,6 +165,24 @@ def tls_certificate_fingerprint_sha256(tls_certificate_pem_bytes): return sha256(tls_cert_der).digest() +@pytest.fixture +async def loop_debug_mode() -> AsyncIterator[None]: + """Enable asyncio debug mode for the duration of the test. + + Disables debug before teardown so PyPy 3.11's recursive + ``Task.__repr__``, triggered by the asyncio slow-callback + logger during connector close, does not surface a spurious + ``RuntimeWarning: coroutine ... was never awaited`` while + the ``aiohttp_client`` fixture finalizes. + """ + loop = asyncio.get_running_loop() + loop.set_debug(True) + try: + yield + finally: + loop.set_debug(False) + + @pytest.fixture def unix_sockname(tmp_path, tmp_path_factory): """Generate an fs path to the UNIX domain socket for testing. diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py index d429e7ceaa0..c50ceaad4c9 100644 --- a/tests/test_web_functional.py +++ b/tests/test_web_functional.py @@ -92,8 +92,9 @@ async def handler(request): await resp.release() -async def test_handler_returns_not_response(aiohttp_server, aiohttp_client) -> None: - asyncio.get_event_loop().set_debug(True) +async def test_handler_returns_not_response( + aiohttp_server, aiohttp_client, loop_debug_mode: None +) -> None: logger = mock.Mock() async def handler(request): @@ -108,8 +109,9 @@ async def handler(request): assert resp.status == 500 -async def test_handler_returns_none(aiohttp_server, aiohttp_client) -> None: - asyncio.get_event_loop().set_debug(True) +async def test_handler_returns_none( + aiohttp_server, aiohttp_client, loop_debug_mode: None +) -> None: logger = mock.Mock() async def handler(request): From ca7d8730687c9c7ec9e304fee7eb2207f517af69 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 May 2026 18:12:37 -0700 Subject: [PATCH 110/191] [PR #12597/28d1214f backport][3.13] Stop calling socket.getfqdn() in BaseRequest.host fallback (#12604) Closes #9308. --- CHANGES/12597.breaking.rst | 1 + CHANGES/9308.breaking.rst | 12 +++++++ aiohttp/test_utils.py | 10 ++++-- aiohttp/web_protocol.py | 11 ++++++ aiohttp/web_request.py | 18 ++++++++-- docs/web_reference.rst | 11 +++++- tests/test_web_request.py | 69 ++++++++++++++++++++++++++++++++------ 7 files changed, 115 insertions(+), 17 deletions(-) create mode 120000 CHANGES/12597.breaking.rst create mode 100644 CHANGES/9308.breaking.rst diff --git a/CHANGES/12597.breaking.rst b/CHANGES/12597.breaking.rst new file mode 120000 index 00000000000..651891d5065 --- /dev/null +++ b/CHANGES/12597.breaking.rst @@ -0,0 +1 @@ +9308.breaking.rst \ No newline at end of file diff --git a/CHANGES/9308.breaking.rst b/CHANGES/9308.breaking.rst new file mode 100644 index 00000000000..afb3965ab8f --- /dev/null +++ b/CHANGES/9308.breaking.rst @@ -0,0 +1,12 @@ +Stopped calling :func:`socket.getfqdn` as the fallback for +:attr:`aiohttp.web.BaseRequest.host`. :func:`socket.getfqdn` +performs blocking reverse DNS resolution on the event loop +thread and can stall a worker for many seconds when the system +resolver is slow, and could be triggered remotely by an HTTP/1.0 +request that omits the ``Host`` header. The fallback when no +``Host`` header is present is now the local socket address the +request arrived on (transport ``sockname``), or an empty string +if no transport information is available. Code that relied on +the FQDN being returned must now read it from +:func:`socket.getfqdn` directly, off the event loop +-- by :user:`bdraco`. diff --git a/aiohttp/test_utils.py b/aiohttp/test_utils.py index 34b77d47cd5..4f38e965e16 100644 --- a/aiohttp/test_utils.py +++ b/aiohttp/test_utils.py @@ -19,8 +19,10 @@ Iterator, List, Optional, + Tuple, Type, TypeVar, + Union, cast, overload, ) @@ -651,11 +653,10 @@ def set_dict(app: Any, key: str, value: Any) -> None: def _create_transport(sslcontext: Optional[SSLContext] = None) -> mock.Mock: transport = mock.Mock() - def get_extra_info(key: str) -> Optional[SSLContext]: + def get_extra_info(key: str) -> Optional[Union[SSLContext, Tuple[str, int]]]: if key == "sslcontext": return sslcontext - else: - return None + return ("127.0.0.1", 80) if key == "sockname" else None transport.get_extra_info.side_effect = get_extra_info return transport @@ -736,6 +737,9 @@ def make_mocked_request( type(protocol).peername = mock.PropertyMock( return_value=transport.get_extra_info("peername") ) + type(protocol).sockname = mock.PropertyMock( + return_value=transport.get_extra_info("sockname") + ) type(protocol).ssl_context = mock.PropertyMock(return_value=sslcontext) if writer is sentinel: diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py index 84e70e59088..b4056a639bc 100644 --- a/aiohttp/web_protocol.py +++ b/aiohttp/web_protocol.py @@ -288,6 +288,17 @@ def peername( else self.transport.get_extra_info("peername") ) + @under_cached_property + def sockname( + self, + ) -> Optional[Union[str, Tuple[str, int, int, int], Tuple[str, int]]]: + """Return sockname if available.""" + return ( + None + if self.transport is None + else self.transport.get_extra_info("sockname") + ) + @property def keepalive_timeout(self) -> float: return self._keepalive_timeout diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py index f2d4c511e44..e02b99f2793 100644 --- a/aiohttp/web_request.py +++ b/aiohttp/web_request.py @@ -2,7 +2,6 @@ import datetime import io import re -import socket import string import tempfile import types @@ -200,6 +199,7 @@ def __init__( self._transport_sslcontext = protocol.ssl_context self._transport_peername = protocol.peername + self._transport_sockname = protocol.sockname if remote is not None: self._cache["remote"] = remote @@ -426,7 +426,9 @@ def host(self) -> str: - overridden value by .clone(host=new_host) call. - HOST HTTP header - - socket.getfqdn() value + - local socket address the request arrived on + (transport ``sockname``) + - empty string if no transport information is available For example, 'example.com' or 'localhost:8080'. @@ -435,7 +437,17 @@ def host(self) -> str: host = self._message.headers.get(hdrs.HOST) if host is not None: return host - return socket.getfqdn() + sockname = self._transport_sockname + if sockname is None: + return "" + if isinstance(sockname, tuple): + # AF_INET6 returns a 4-tuple (host, port, flowinfo, scopeid); + # bracket the bare address so it matches the Host-header shape + # and is a valid URL authority component. + if len(sockname) == 4: + return f"[{sockname[0]}]" + return str(sockname[0]) + return str(sockname) @reify def remote(self) -> Optional[str]: diff --git a/docs/web_reference.rst b/docs/web_reference.rst index 83a9c7f0ecd..c0e8b983538 100644 --- a/docs/web_reference.rst +++ b/docs/web_reference.rst @@ -118,7 +118,9 @@ and :ref:`aiohttp-web-signals` handlers. - Overridden value by :meth:`~BaseRequest.clone` call. - *Host* HTTP header - - :func:`socket.getfqdn` + - local socket address the request arrived on + (transport ``sockname``) + - empty string if no transport information is available Read-only :class:`str` property. @@ -129,6 +131,13 @@ and :ref:`aiohttp-web-signals` handlers. Call ``.clone(host=new_host)`` for setting up the value explicitly. + .. versionchanged:: 3.13 + + The fallback when no ``Host`` header is present no longer + calls :func:`socket.getfqdn`, which performed blocking + reverse-DNS resolution on the event loop. The local socket + address (transport ``sockname``) is used instead. + .. seealso:: :ref:`aiohttp-web-forwarded-support` .. attribute:: remote diff --git a/tests/test_web_request.py b/tests/test_web_request.py index 7778128c19e..dfbfb369732 100644 --- a/tests/test_web_request.py +++ b/tests/test_web_request.py @@ -1,7 +1,6 @@ import asyncio import datetime import logging -import socket import time from collections.abc import MutableMapping from typing import Any @@ -40,16 +39,19 @@ def test_base_ctor() -> None: URL("/path/to?a=1&b=2"), ) + protocol = mock.Mock() + protocol.ssl_context = None + protocol.peername = None + protocol.sockname = ("127.0.0.1", 80) req = BaseRequest( - message, mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock() + message, mock.Mock(), protocol, mock.Mock(), mock.Mock(), mock.Mock() ) assert "GET" == req.method assert HttpVersion(1, 1) == req.version - # MacOS may return CamelCased host name, need .lower() - # FQDN can be wider than host, e.g. - # 'fv-az397-495' in 'fv-az397-495.internal.cloudapp.net' - assert req.host.lower() in socket.getfqdn().lower() + # No Host header in this request, so host falls back to the + # local socket address the request arrived on. + assert req.host == "127.0.0.1" assert "/path/to?a=1&b=2" == req.path_qs assert "/path/to" == req.path assert "a=1&b=2" == req.query_string @@ -71,10 +73,10 @@ def test_ctor() -> None: assert "GET" == req.method assert HttpVersion(1, 1) == req.version - # MacOS may return CamelCased host name, need .lower() - # FQDN can be wider than host, e.g. - # 'fv-az397-495' in 'fv-az397-495.internal.cloudapp.net' - assert req.host.lower() in socket.getfqdn().lower() + # No Host header in this request, so host falls back to the + # local socket address the request arrived on (the default + # sockname configured by make_mocked_request). + assert req.host == "127.0.0.1" assert "/path/to?a=1&b=2" == req.path_qs assert "/path/to" == req.path assert "a=1&b=2" == req.query_string @@ -116,6 +118,53 @@ def test_deprecated_message() -> None: assert req.message == req._message +def test_host_falls_back_to_sockname_not_dns() -> None: + """Regression: request.host must not call socket.getfqdn(). + + socket.getfqdn() does blocking reverse DNS resolution on the + event loop thread and can stall a worker for many seconds when + the system resolver is slow. The fallback for a request with no + Host header is the local socket address the request arrived on, + not the system FQDN. + """ + req = make_mocked_request("GET", "/") + assert req.host == "127.0.0.1" + assert str(req.url).startswith("http://127.0.0.1") + + +def test_host_with_ipv6_sockname() -> None: + """AF_INET6 sockname is bracketed to form a valid URL authority. + + A bare IPv6 string would cause ``URL.build(authority=...)`` to + raise ``ValueError``. + """ + transport = mock.Mock() + transport.get_extra_info.side_effect = lambda key: ( + ("::1", 80, 0, 0) if key == "sockname" else None + ) + req = make_mocked_request("GET", "/", transport=transport) + assert req.host == "[::1]" + assert str(req.url) == "http://[::1]/" + + +def test_host_with_unix_socket_sockname() -> None: + """Unix-socket transports expose sockname as a str path.""" + transport = mock.Mock() + transport.get_extra_info.side_effect = lambda key: ( + "/tmp/aiohttp.sock" if key == "sockname" else None + ) + req = make_mocked_request("GET", "/", transport=transport) + assert req.host == "/tmp/aiohttp.sock" + + +def test_host_with_no_transport_sockname() -> None: + """An empty string is returned when no sockname is available.""" + transport = mock.Mock() + transport.get_extra_info.return_value = None + req = make_mocked_request("GET", "/", transport=transport) + assert req.host == "" + + def test_doubleslashes() -> None: # NB: //foo/bar is an absolute URL with foo netloc and /bar path req = make_mocked_request("GET", "/bar//foo/") From f57a6ab518c8967027d1db17253b003f6f209f80 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 May 2026 18:29:31 -0700 Subject: [PATCH 111/191] [PR #12603/dbe83fd0 backport][3.13] Fix flaky test_handler_returns_not_response on PyPy via debug-mode fixture (#12607) --- CHANGES/12603.contrib.rst | 8 ++++++++ tests/conftest.py | 18 ++++++++++++++++++ tests/test_web_functional.py | 10 ++++++---- 3 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 CHANGES/12603.contrib.rst diff --git a/CHANGES/12603.contrib.rst b/CHANGES/12603.contrib.rst new file mode 100644 index 00000000000..51574d02868 --- /dev/null +++ b/CHANGES/12603.contrib.rst @@ -0,0 +1,8 @@ +Fixed flaky ``test_handler_returns_not_response`` and +``test_handler_returns_none`` by routing ``loop.set_debug(True)`` +through a new ``loop_debug_mode`` fixture that disables debug +mode before the ``aiohttp_client`` fixture finalizes. Leaving +debug on through teardown let PyPy 3.11's asyncio slow-callback +logger walk into ``Task.__repr__`` during connector close, +surfacing a spurious ``RuntimeWarning: coroutine was never +awaited`` -- by :user:`bdraco`. diff --git a/tests/conftest.py b/tests/conftest.py index c299df5aa3f..cccf267941d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -160,6 +160,24 @@ def tls_certificate_fingerprint_sha256(tls_certificate_pem_bytes): return sha256(tls_cert_der).digest() +@pytest.fixture +async def loop_debug_mode() -> AsyncIterator[None]: + """Enable asyncio debug mode for the duration of the test. + + Disables debug before teardown so PyPy 3.11's recursive + ``Task.__repr__``, triggered by the asyncio slow-callback + logger during connector close, does not surface a spurious + ``RuntimeWarning: coroutine ... was never awaited`` while + the ``aiohttp_client`` fixture finalizes. + """ + loop = asyncio.get_running_loop() + loop.set_debug(True) + try: + yield + finally: + loop.set_debug(False) + + @pytest.fixture def unix_sockname(tmp_path, tmp_path_factory): """Generate an fs path to the UNIX domain socket for testing. diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py index fe9cce27b8e..089b183858a 100644 --- a/tests/test_web_functional.py +++ b/tests/test_web_functional.py @@ -91,8 +91,9 @@ async def handler(request): await resp.release() -async def test_handler_returns_not_response(aiohttp_server, aiohttp_client) -> None: - asyncio.get_event_loop().set_debug(True) +async def test_handler_returns_not_response( + aiohttp_server, aiohttp_client, loop_debug_mode: None +) -> None: logger = mock.Mock() async def handler(request): @@ -107,8 +108,9 @@ async def handler(request): assert resp.status == 500 -async def test_handler_returns_none(aiohttp_server, aiohttp_client) -> None: - asyncio.get_event_loop().set_debug(True) +async def test_handler_returns_none( + aiohttp_server, aiohttp_client, loop_debug_mode: None +) -> None: logger = mock.Mock() async def handler(request): From 67bedfd7c93d905a9f10b47aef495939242694ad Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 01:53:47 +0000 Subject: [PATCH 112/191] [PR #12596/91b061b0 backport][3.14] Switch test-results upload to codecov-action (#12599) Co-authored-by: J. Nick Koston --- .github/workflows/ci-cd.yml | 8 ++++++-- CHANGES/12596.contrib.rst | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 CHANGES/12596.contrib.rst diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 06303863c3c..167bd6813a6 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -262,8 +262,10 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} - name: Upload test results to Codecov if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 + uses: codecov/codecov-action@v6 with: + files: ./junit.xml + report_type: test_results token: ${{ secrets.CODECOV_TOKEN }} autobahn: @@ -354,8 +356,10 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} - name: Upload test results to Codecov if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 + uses: codecov/codecov-action@v6 with: + files: ./junit.xml + report_type: test_results token: ${{ secrets.CODECOV_TOKEN }} benchmark: diff --git a/CHANGES/12596.contrib.rst b/CHANGES/12596.contrib.rst new file mode 100644 index 00000000000..ff96abe5bfc --- /dev/null +++ b/CHANGES/12596.contrib.rst @@ -0,0 +1,4 @@ +Switched the test-results upload step to ``codecov/codecov-action@v6`` +with ``report_type: test_results``, since +``codecov/test-results-action`` is being deprecated in favor of +``codecov-action`` -- by :user:`bdraco`. From ee93c07bc0ed83683af22bd1534638332532d0a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 May 2026 19:50:02 -0700 Subject: [PATCH 113/191] [PR #12609/b26cfbff backport][3.14] Fix flaky test_shutdown_handler_cancellation_suppressed (#12612) --- tests/test_run_app.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_run_app.py b/tests/test_run_app.py index f855fbe0a06..d9e21c696c0 100644 --- a/tests/test_run_app.py +++ b/tests/test_run_app.py @@ -1284,6 +1284,7 @@ def test_shutdown_handler_cancellation_suppressed( sock = unused_port_socket port = sock.getsockname()[1] actions = [] + suppressed = asyncio.Event() async def test() -> None: async def test_resp(sess): @@ -1295,7 +1296,8 @@ async def test_resp(sess): async with ClientSession() as sess: t = asyncio.create_task(test_resp(sess)) - await asyncio.sleep(0.5) + # Wait until the handler has observed its cancellation. + await asyncio.wait_for(suppressed.wait(), timeout=3) # Handler is in-progress while we trigger server shutdown. actions.append("PRESTOP") async with sess.get(f"http://127.0.0.1:{port}/stop"): @@ -1316,6 +1318,7 @@ async def handler(request: web.Request) -> web.Response: await asyncio.sleep(5) except asyncio.CancelledError: actions.append("SUPPRESSED") + suppressed.set() await asyncio.sleep(2) actions.append("DONE") return web.Response(text="FOO") From e03870e9291eb201d8411d8beb68ffdd48072cb4 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 18 May 2026 07:55:59 +0100 Subject: [PATCH 114/191] Create AGENTS.md (#12613) --- AGENTS.md | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..ec687201121 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,71 @@ +# AGENTS.md + +This file provides guidance to AI coding agents working with this repository. + +## Branching + +- All new features and bug fixes target `master`. A bot creates backports to the `x.y` release branches automatically. + +## Build + +- Full dev setup: `make install-dev` (installs deps, cythonizes, builds extensions) +- Vendored llhttp: `git submodule update --init` + `make generate-llhttp` (requires Node.js) +- Cython extensions: `make cythonize` (.pyx → .c), then `pip install -e .` to compile +- Pure Python mode: `AIOHTTP_NO_EXTENSIONS=1 pip install -e .` +- `AIOHTTP_CYTHON_TRACE=1` enables Cython trace macros (only useful with linetrace-enabled .c files) + +## Test + +- Run all: `PYTHONPATH='.' pytest --numprocesses=auto` +- Single test: `PYTHONPATH='.' pytest tests/test_foo.py::test_name` +- Pure Python only: `PYTHONPATH='.' AIOHTTP_NO_EXTENSIONS=1 pytest` + +## Lint & Format + +- `pre-commit run --all-files` runs all checks (black, isort, flake8, pyupgrade, codespell) +- `black` for formatting only, `mypy` for type checking +- black with 88-col line length, isort with trailing commas + +## Documentation & code style + +- User-visible API changes need a docs update under `docs/` (`docs/client_reference.rst` / `docs/web_reference.rst` plus any narrative pages). +- Docstrings in code, prose in Sphinx; `make doc` builds locally. +- No docstrings or comments that just restate the code. + +## Changelog + +- Fragments in `CHANGES/{pr_or_issue}.{type}.rst` (valid types are defined in `pyproject.toml` under `[tool.towncrier]`). +- Sign with `` -- by :user:`github-handle` ``. +- Both issue and PR number wanted: keep the issue-numbered file and symlink: `ln -s 1234.bugfix.rst CHANGES/1240.bugfix.rst`. +- Multiple fragments same category: `1234.feature.rst`, `1234.feature.1.rst`. + +## PRs + +- **Prove it works before opening the PR**. This means: + - Relevant tests pass locally. + - New behaviour is covered by a test. + - Any parser/websocket related changes have been tested with Cython extensions installed. +- Use the shipped template at [`.github/PULL_REQUEST_TEMPLATE.md`](.github/PULL_REQUEST_TEMPLATE.md). + - A couple of sentences per section is plenty. + - Tick checklist boxes that apply; write `N/A` next to ones that do not. + - First-time contributors add themselves to `CONTRIBUTORS.txt` (alphabetical by first name). +- **Draft.** Use `gh pr create --draft`. + - Every submission must be reviewed by a human before going out of draft; that review is the operator's job, not the maintainers'. + - Do not mark ready or request reviewers yourself. +- **Disclosure.** One plain line at the bottom: `Drafted with ; reviewed by .` +- **No `Co-Authored-By:`** LLM trailers in commits or PR body. +- Agent run output (test logs) goes in a collapsed `
` block **below** the template summary. + +**Commits**: Don't use conventional commits; match recent imperative subjects. + +## Threat model + +`THREAT_MODEL.md` is a living document and should be revised when: + +- A CVE / GHSA is filed against aiohttp. +- The parser configuration changes (llhttp lenient flags, size limits, version regex). +- Any default referenced in the document changes (`client_max_size`, `keepalive_timeout`, `max_redirects`, `limit`, `limit_per_host`, etc.). +- The vendored llhttp version is bumped. +- A public API surface is added or removed in `client.py` / `web_*.py` / `multipart.py`. + +When a chunk's content is materially affected, update both the chunk and the relevant entries in §6.1–§6.4. The "Past advisories / hardening (recap)" subsection of each chunk is the audit trail for what has been verified-in-place. From 3427840086f831564920652845fcce7461c0ccc3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 May 2026 00:23:06 -0700 Subject: [PATCH 115/191] [PR #12600/f402c3d8 backport][3.14] Silence uvloop 0.22+ asyncio.AbstractEventLoopPolicy deprecation (#12616) --- CHANGES/12600.contrib.rst | 4 ++++ setup.cfg | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 CHANGES/12600.contrib.rst diff --git a/CHANGES/12600.contrib.rst b/CHANGES/12600.contrib.rst new file mode 100644 index 00000000000..4d361389191 --- /dev/null +++ b/CHANGES/12600.contrib.rst @@ -0,0 +1,4 @@ +Silenced the ``DeprecationWarning`` raised by ``uvloop`` 0.22+ when it +accesses the ``asyncio.AbstractEventLoopPolicy`` alias that Python 3.14 +marks for removal in 3.16, so the test suite passes against the newer +``uvloop`` release -- by :user:`bdraco`. diff --git a/setup.cfg b/setup.cfg index 5df4444e507..6e82d8c00bd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -82,6 +82,10 @@ filterwarnings = ignore:coroutine method 'aclose' of 'BodyPartReader._decode_content_async' was never awaited:RuntimeWarning # Our own warning ignore:aiohttp.pytest_plugin will be removed in v4:DeprecationWarning + # uvloop 0.22+ accesses the deprecated asyncio.AbstractEventLoopPolicy + # alias, which Python 3.14 marks for removal in 3.16. Drop this when + # uvloop stops touching the deprecated alias. + ignore:'asyncio.AbstractEventLoopPolicy' is deprecated:DeprecationWarning:uvloop junit_suite_name = aiohttp_test_suite norecursedirs = dist docs build .tox .eggs minversion = 3.8.2 From 0503dbd0e644718006b2f4f4e77686e1b7c521ae Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 07:38:14 -0700 Subject: [PATCH 116/191] [PR #12619/b391f7be backport][3.14] Add timeouts to test runners to prevent CI hangs (#12622) Co-authored-by: J. Nick Koston --- .github/workflows/ci-cd.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 167bd6813a6..7344bbc2efe 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -167,6 +167,7 @@ jobs: fail-fast: true runs-on: ${{ matrix.os }}-latest continue-on-error: ${{ matrix.experimental }} + timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@v6 @@ -281,6 +282,7 @@ jobs: os: [ubuntu] fail-fast: true runs-on: ${{ matrix.os }}-latest + timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@v6 @@ -416,6 +418,7 @@ jobs: matrix: os: [ubuntu, windows] runs-on: ${{ matrix.os }}-latest + timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@v6 From cecad51bf65c144e27790e6991c1881749d16a07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 May 2026 07:45:48 -0700 Subject: [PATCH 117/191] [PR #12619/b391f7be backport][3.13] Add timeouts to test runners to prevent CI hangs (#12623) --- .github/workflows/ci-cd.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 0ae7a7eb1ed..4963ce47324 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -167,6 +167,7 @@ jobs: fail-fast: true runs-on: ${{ matrix.os }}-latest continue-on-error: ${{ matrix.experimental }} + timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@v5 From 02558d38a94fc251ef7ca2a25b2deecfe1e3c9b7 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 15:27:32 +0000 Subject: [PATCH 118/191] [PR #12606/3db06d9c backport][3.14] Shrink three slow tests without changing their intent (#12625) Co-authored-by: J. Nick Koston --- CHANGES/12606.contrib.rst | 6 ++++++ tests/test_client_functional.py | 8 ++++---- tests/test_http_parser.py | 8 +++++--- 3 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 CHANGES/12606.contrib.rst diff --git a/CHANGES/12606.contrib.rst b/CHANGES/12606.contrib.rst new file mode 100644 index 00000000000..72925114e8c --- /dev/null +++ b/CHANGES/12606.contrib.rst @@ -0,0 +1,6 @@ +Reduced runtime of several of the slowest unit tests +(decompress size-limit payloads from 64 MiB to 2 MiB, +``test_chunk_splits_after_pause`` chunk count from 50000 +to 20000, and ``test_set_cookies_max_age`` sleep from 2 +seconds to 1.1 seconds) without changing what they +exercise -- by :user:`bdraco`. diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 4b4d12443b6..802b8ee239d 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -2460,7 +2460,7 @@ async def test_payload_decompress_size_limit(aiohttp_client: AiohttpClient) -> N we raise DecompressSizeError. """ # Create a highly compressible payload. - payload_size = 64 * 2**20 + payload_size = 2 * 2**20 original = b"A" * payload_size compressed = zlib.compress(original) assert len(original) > DEFAULT_CHUNK_SIZE @@ -2492,7 +2492,7 @@ async def test_payload_decompress_size_limit_brotli( """Test that brotli decompression size limit triggers DecompressSizeError.""" assert brotli is not None # Create a highly compressible payload - payload_size = 64 * 2**20 + payload_size = 2 * 2**20 original = b"A" * payload_size compressed = brotli.compress(original) assert len(original) > DEFAULT_CHUNK_SIZE @@ -2523,7 +2523,7 @@ async def test_payload_decompress_size_limit_zstd( """Test that zstd decompression size limit triggers DecompressSizeError.""" assert ZstdCompressor is not None # Create a highly compressible payload. - payload_size = 64 * 2**20 + payload_size = 2 * 2**20 original = b"A" * payload_size compressor = ZstdCompressor() compressed = compressor.compress(original) + compressor.flush() @@ -2930,7 +2930,7 @@ async def handler(request): assert 200 == resp.status cookie_names = {c.key for c in client.session.cookie_jar} assert cookie_names == {"c1", "c2", "c3"} - await asyncio.sleep(2) + await asyncio.sleep(1.1) cookie_names = {c.key for c in client.session.cookie_jar} assert cookie_names == {"c1", "c2"} diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index c3a8669cb31..99e952ac882 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -1074,9 +1074,10 @@ def test_max_header_value_size_under_limit(parser: HttpRequestParser) -> None: async def test_chunk_splits_after_pause(parser: HttpRequestParser) -> None: + num_chunks = 20000 # comfortably above the 16385 pause threshold text = ( b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\n\r\n" - + b"1\r\nb\r\n" * 50000 + + b"1\r\nb\r\n" * num_chunks + b"0\r\n\r\n" ) @@ -1087,8 +1088,9 @@ async def test_chunk_splits_after_pause(parser: HttpRequestParser) -> None: assert len(payload._http_chunk_splits) == 16385 # We should still get the full result after read(), as it will continue processing. result = await payload.read() - assert len(result) == 50000 # Compare len first, as it's easier to debug in diff. - assert result == b"b" * 50000 + # Compare len first, as it's easier to debug in diff. + assert len(result) == num_chunks + assert result == b"b" * num_chunks async def test_compressed_with_tail(response: HttpResponseParser) -> None: From 869fd3c66de7eed04e0c76a950620c73e016a4d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 May 2026 08:34:26 -0700 Subject: [PATCH 119/191] [PR #12624/3bca18a8 backport][3.14] Add per-test pytest-timeout to surface hung tests (#12627) --- .github/workflows/ci-cd.yml | 4 ++-- CHANGES/12624.contrib.rst | 4 ++++ requirements/test-common.in | 1 + requirements/test-common.txt | 2 ++ requirements/test-ft.txt | 2 ++ requirements/test.txt | 2 ++ setup.cfg | 3 +++ 7 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 CHANGES/12624.contrib.rst diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 7344bbc2efe..eea8db6665e 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -335,7 +335,7 @@ jobs: PIP_USER: 1 run: >- PATH="${HOME}/Library/Python/3.11/bin:${HOME}/.local/bin:${PATH}" - pytest --junitxml=junit.xml --cov=aiohttp/ --cov=tests/ -m autobahn + pytest --junitxml=junit.xml --cov=aiohttp/ --cov=tests/ --timeout=0 -m autobahn shell: bash - name: Turn coverage into xml env: @@ -405,7 +405,7 @@ jobs: uses: CodSpeedHQ/action@v4 with: mode: instrumentation - run: python -Im pytest --no-cov -vvvvv --codspeed --durations=30 + run: python -Im pytest --no-cov -vvvvv --codspeed --durations=30 --timeout=0 cython-coverage: diff --git a/CHANGES/12624.contrib.rst b/CHANGES/12624.contrib.rst new file mode 100644 index 00000000000..0c7e2a0a548 --- /dev/null +++ b/CHANGES/12624.contrib.rst @@ -0,0 +1,4 @@ +Added a default 120-second per-test timeout via ``pytest-timeout`` so a +hung test surfaces by name in CI output instead of getting hidden behind +the job-level timeout added in :pr:`12619`. The ``autobahn`` and +benchmark jobs opt out with ``--timeout=0`` -- by :user:`bdraco`. diff --git a/requirements/test-common.in b/requirements/test-common.in index b2f6dd869e1..77700881024 100644 --- a/requirements/test-common.in +++ b/requirements/test-common.in @@ -8,6 +8,7 @@ proxy.py >= 2.4.4rc5 pytest pytest-cov pytest-mock +pytest-timeout pytest-xdist pytest_codspeed python-on-whales diff --git a/requirements/test-common.txt b/requirements/test-common.txt index f0234b57ba7..bf54902928a 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -79,6 +79,8 @@ pytest-cov==7.1.0 # via -r requirements/test-common.in pytest-mock==3.15.1 # via -r requirements/test-common.in +pytest-timeout==2.4.0 + # via -r requirements/test-common.in pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index baf5c381d3f..309efbaf5bd 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -115,6 +115,8 @@ pytest-cov==7.1.0 # via -r requirements/test-common.in pytest-mock==3.15.1 # via -r requirements/test-common.in +pytest-timeout==2.4.0 + # via -r requirements/test-common.in pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 diff --git a/requirements/test.txt b/requirements/test.txt index 4ea290d8e2c..9df6d8f9e00 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -115,6 +115,8 @@ pytest-cov==7.1.0 # via -r requirements/test-common.in pytest-mock==3.15.1 # via -r requirements/test-common.in +pytest-timeout==2.4.0 + # via -r requirements/test-common.in pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 diff --git a/setup.cfg b/setup.cfg index 6e82d8c00bd..6ea9b485db6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -54,6 +54,9 @@ addopts = --showlocals -m "not dev_mode and not autobahn and not internal" +# 2-minute per-test timeout so a hung test surfaces by name instead of taking +# down the whole job. Autobahn and benchmark jobs override with `--timeout=0`. +timeout = 120 filterwarnings = error ignore:module 'ssl' has no attribute 'OP_NO_COMPRESSION'. The Python interpreter is compiled against OpenSSL < 1.0.0. Ref. https.//docs.python.org/3/library/ssl.html#ssl.OP_NO_COMPRESSION:UserWarning From 4ec3ac37ad25139af1d6156ecd0cfd4d3e84050e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 May 2026 08:36:49 -0700 Subject: [PATCH 120/191] [PR #12624/3bca18a8 backport][3.13] Add per-test pytest-timeout to surface hung tests (#12626) --- .github/workflows/ci-cd.yml | 2 +- CHANGES/12624.contrib.rst | 4 ++++ requirements/test-common.in | 1 + requirements/test-common.txt | 2 ++ requirements/test-ft.txt | 2 ++ requirements/test.txt | 2 ++ setup.cfg | 3 +++ 7 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 CHANGES/12624.contrib.rst diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 4963ce47324..aec0cfac0f0 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -307,7 +307,7 @@ jobs: uses: CodSpeedHQ/action@v4 with: mode: instrumentation - run: python -Im pytest --no-cov --numprocesses=0 -vvvvv --codspeed + run: python -Im pytest --no-cov --numprocesses=0 -vvvvv --codspeed --timeout=0 check: # This job does nothing and is only used for the branch protection diff --git a/CHANGES/12624.contrib.rst b/CHANGES/12624.contrib.rst new file mode 100644 index 00000000000..0c7e2a0a548 --- /dev/null +++ b/CHANGES/12624.contrib.rst @@ -0,0 +1,4 @@ +Added a default 120-second per-test timeout via ``pytest-timeout`` so a +hung test surfaces by name in CI output instead of getting hidden behind +the job-level timeout added in :pr:`12619`. The ``autobahn`` and +benchmark jobs opt out with ``--timeout=0`` -- by :user:`bdraco`. diff --git a/requirements/test-common.in b/requirements/test-common.in index b2f6dd869e1..77700881024 100644 --- a/requirements/test-common.in +++ b/requirements/test-common.in @@ -8,6 +8,7 @@ proxy.py >= 2.4.4rc5 pytest pytest-cov pytest-mock +pytest-timeout pytest-xdist pytest_codspeed python-on-whales diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 16c3a976898..8159a92c22c 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -77,6 +77,8 @@ pytest-cov==7.0.0 # via -r requirements/test-common.in pytest-mock==3.15.1 # via -r requirements/test-common.in +pytest-timeout==2.4.0 + # via -r requirements/test-common.in pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 18abb27d9cf..b904ec6c8ed 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -110,6 +110,8 @@ pytest-cov==7.0.0 # via -r requirements/test-common.in pytest-mock==3.15.1 # via -r requirements/test-common.in +pytest-timeout==2.4.0 + # via -r requirements/test-common.in pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 diff --git a/requirements/test.txt b/requirements/test.txt index 52b74930a05..f71af7ba99c 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -110,6 +110,8 @@ pytest-cov==7.0.0 # via -r requirements/test-common.in pytest-mock==3.15.1 # via -r requirements/test-common.in +pytest-timeout==2.4.0 + # via -r requirements/test-common.in pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 diff --git a/setup.cfg b/setup.cfg index eb3a36b9d23..76079e364a4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,6 +63,9 @@ addopts = # run tests that are not marked with dev_mode -m "not dev_mode" +# 2-minute per-test timeout so a hung test surfaces by name instead of taking +# down the whole job. Autobahn and benchmark jobs override with `--timeout=0`. +timeout = 120 filterwarnings = error ignore:module 'ssl' has no attribute 'OP_NO_COMPRESSION'. The Python interpreter is compiled against OpenSSL < 1.0.0. Ref. https.//docs.python.org/3/library/ssl.html#ssl.OP_NO_COMPRESSION:UserWarning From e439180a7ec7569262ba23bf545f5c05a6ed8a84 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 17:05:46 +0000 Subject: [PATCH 121/191] Bump uvloop from 0.21.0 to 0.22.1 (#11683) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [uvloop](https://github.com/MagicStack/uvloop) from 0.21.0 to 0.22.1.
Release notes

Sourced from uvloop's releases.

v0.22.1

This is identical to 0.22.0, re-ran with CI fixes

v0.22.0

Changes

Fixes

Commits

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/base.txt | 2 +- requirements/constraints.txt | 5 ++++- requirements/dev.txt | 5 ++++- requirements/lint.txt | 2 +- requirements/test.txt | 3 ++- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 20730032d38..c1abe470372 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -47,7 +47,7 @@ typing-extensions==4.15.0 ; python_version < "3.13" # -r requirements/runtime-deps.in # aiosignal # multidict -uvloop==0.21.0 ; platform_system != "Windows" and implementation_name == "cpython" +uvloop==0.22.1 ; platform_system != "Windows" and implementation_name == "cpython" # via -r requirements/base.in yarl==1.22.0 # via -r requirements/runtime-deps.in diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 95ea5248658..baa98a1a7c4 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -183,6 +183,7 @@ pytest==9.0.3 # pytest-codspeed # pytest-cov # pytest-mock + # pytest-timeout # pytest-xdist pytest-codspeed==5.0.2 # via @@ -194,6 +195,8 @@ pytest-mock==3.15.1 # via # -r requirements/lint.in # -r requirements/test-common.in +pytest-timeout==2.4.0 + # via -r requirements/test-common.in pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 @@ -280,7 +283,7 @@ typing-inspection==0.4.2 # via pydantic urllib3==2.7.0 # via requests -uvloop==0.21.0 ; platform_system != "Windows" +uvloop==0.22.1 ; platform_system != "Windows" # via # -r requirements/base.in # -r requirements/lint.in diff --git a/requirements/dev.txt b/requirements/dev.txt index eabac796cb9..40e3f3ef30f 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -178,6 +178,7 @@ pytest==9.0.3 # pytest-codspeed # pytest-cov # pytest-mock + # pytest-timeout # pytest-xdist pytest-codspeed==5.0.2 # via @@ -189,6 +190,8 @@ pytest-mock==3.15.1 # via # -r requirements/lint.in # -r requirements/test-common.in +pytest-timeout==2.4.0 + # via -r requirements/test-common.in pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 @@ -270,7 +273,7 @@ typing-inspection==0.4.2 # via pydantic urllib3==2.7.0 # via requests -uvloop==0.21.0 ; platform_system != "Windows" and implementation_name == "cpython" +uvloop==0.22.1 ; platform_system != "Windows" and implementation_name == "cpython" # via # -r requirements/base.in # -r requirements/lint.in diff --git a/requirements/lint.txt b/requirements/lint.txt index ef45375b634..6b0be6d5a85 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -124,7 +124,7 @@ typing-extensions==4.15.0 # virtualenv typing-inspection==0.4.2 # via pydantic -uvloop==0.21.0 ; platform_system != "Windows" +uvloop==0.22.1 ; platform_system != "Windows" # via -r requirements/lint.in valkey==6.1.1 # via -r requirements/lint.in diff --git a/requirements/test.txt b/requirements/test.txt index 9df6d8f9e00..bc065a27805 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -108,6 +108,7 @@ pytest==9.0.3 # pytest-codspeed # pytest-cov # pytest-mock + # pytest-timeout # pytest-xdist pytest-codspeed==5.0.2 # via -r requirements/test-common.in @@ -154,7 +155,7 @@ typing-extensions==4.15.0 ; python_version < "3.13" # typing-inspection typing-inspection==0.4.2 # via pydantic -uvloop==0.21.0 ; platform_system != "Windows" and implementation_name == "cpython" +uvloop==0.22.1 ; platform_system != "Windows" and implementation_name == "cpython" # via -r requirements/base.in wait-for-it==2.3.0 # via -r requirements/test-common.in From 1b5c21e0ab55be733e12d547fbb2206e9f719ed9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 20:21:20 +0000 Subject: [PATCH 122/191] Bump ast-serialize from 0.4.0 to 0.5.0 (#12634) Bumps [ast-serialize](https://github.com/mypyc/ast_serialize) from 0.4.0 to 0.5.0.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ast-serialize&package-manager=pip&previous-version=0.4.0&new-version=0.5.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 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-common.txt | 3 ++- requirements/test-ft.txt | 3 ++- requirements/test.txt | 2 +- 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index baa98a1a7c4..c3c02cccd85 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -18,7 +18,7 @@ alabaster==1.0.0 # via sphinx annotated-types==0.7.0 # via pydantic -ast-serialize==0.4.0 +ast-serialize==0.5.0 # via mypy async-timeout==5.0.1 ; python_version < "3.11" # via diff --git a/requirements/dev.txt b/requirements/dev.txt index 40e3f3ef30f..48699e31765 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -18,7 +18,7 @@ alabaster==1.0.0 # via sphinx annotated-types==0.7.0 # via pydantic -ast-serialize==0.4.0 +ast-serialize==0.5.0 # via mypy async-timeout==5.0.1 ; python_version < "3.11" # via diff --git a/requirements/lint.txt b/requirements/lint.txt index 6b0be6d5a85..47865d7d125 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -8,7 +8,7 @@ aiodns==4.0.0 # via -r requirements/lint.in annotated-types==0.7.0 # via pydantic -ast-serialize==0.4.0 +ast-serialize==0.5.0 # via mypy async-timeout==5.0.1 # via valkey diff --git a/requirements/test-common.txt b/requirements/test-common.txt index bf54902928a..2fa57883159 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -6,7 +6,7 @@ # annotated-types==0.7.0 # via pydantic -ast-serialize==0.4.0 +ast-serialize==0.5.0 # via mypy blockbuster==1.5.26 # via -r requirements/test-common.in @@ -72,6 +72,7 @@ pytest==9.0.3 # pytest-codspeed # pytest-cov # pytest-mock + # pytest-timeout # pytest-xdist pytest-codspeed==5.0.2 # via -r requirements/test-common.in diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 309efbaf5bd..27b43324777 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -12,7 +12,7 @@ aiosignal==1.4.0 # via -r requirements/runtime-deps.in annotated-types==0.7.0 # via pydantic -ast-serialize==0.4.0 +ast-serialize==0.5.0 # via mypy async-timeout==5.0.1 ; python_version < "3.11" # via -r requirements/runtime-deps.in @@ -108,6 +108,7 @@ pytest==9.0.3 # pytest-codspeed # pytest-cov # pytest-mock + # pytest-timeout # pytest-xdist pytest-codspeed==5.0.2 # via -r requirements/test-common.in diff --git a/requirements/test.txt b/requirements/test.txt index bc065a27805..ee214f01243 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -12,7 +12,7 @@ aiosignal==1.4.0 # via -r requirements/runtime-deps.in annotated-types==0.7.0 # via pydantic -ast-serialize==0.4.0 +ast-serialize==0.5.0 # via mypy async-timeout==5.0.1 ; python_version < "3.11" # via -r requirements/runtime-deps.in From 38c9057c01b383ebdcac4192e0559647f09b2039 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 20:22:45 +0000 Subject: [PATCH 123/191] Bump click from 8.3.3 to 8.4.0 (#12636) Bumps [click](https://github.com/pallets/click) from 8.3.3 to 8.4.0.
Release notes

Sourced from click's releases.

8.4.0

This is the Click 8.4.0 feature release. A feature release may include new features, remove previously deprecated code, add new deprecation, or introduce potentially breaking changes.

We encourage everyone to upgrade. You can read more about our Version Support Policy on our website.

PyPI: https://pypi.org/project/click/8.4.0/ Changes: https://click.palletsprojects.com/page/changes/#version-8-4-0 Milestone https://github.com/pallets/click/milestone/30

  • ParamType typing improvements. #3371

    • :class:ParamType is now a generic abstract base class, parameterized by its converted value type.
    • :meth:~ParamType.convert return types are narrowed on all concrete types (str for :class:STRING, int for :class:INT, etc.).
    • :meth:~ParamType.to_info_dict returns specific :class:~typing.TypedDict subclasses instead of dict[str, Any].
    • :class:CompositeParamType and the number-range base are now generic with abstract methods.
  • Refactor convert_type to extract type inference into a private _guess_type helper, and add :func:typing.overload signatures. #3372

  • Parameter typing improvements. #2805

    • :class:Parameter is now an abstract base class, making explicit that it cannot be instantiated directly.
    • :attr:Parameter.name is now str instead of str | None. When expose_value=False, the name is set to "" instead of None.
    • The ctx parameter of :meth:Parameter.get_error_hint is now typed as Context | None, matching the runtime behavior.
  • Split string values from default_map for parameters with nargs > 1 or :class:Tuple type, matching environment variable behavior. #2745 #3364

  • Auto-detect type=UNPROCESSED for flag_value of non-basic types (not str, int, float, or bool), so programmer-provided Python objects like classes and enum members are passed through unchanged instead of being stringified. Previously type=click.UNPROCESSED had to be set explicitly. #2012 #3363

  • The error hint now uses Command.get_help_option_names to pick non-shadowed help option names, so Try '... -h' no longer points to a subcommand option that shadows -h. All surviving names are shown (-h/--help). #2790 #3208

  • Fix readline functionality on non-Windows platforms. Prompt text is now passed directly to readline instead of being printed separately, allowing proper backspace, line editing, and line wrapping behavior. #2968

... (truncated)

Changelog

Sourced from click's changelog.

Version 8.4.0

Released 2026-05-17

  • :class:ParamType typing improvements. :pr:3371

    • :class:ParamType is now a generic abstract base class, parameterized by its converted value type.
    • :meth:~ParamType.convert return types are narrowed on all concrete types (str for :class:STRING, int for :class:INT, etc.).
    • :meth:~ParamType.to_info_dict returns specific :class:~typing.TypedDict subclasses instead of dict[str, Any].
    • :class:CompositeParamType and the number-range base are now generic with abstract methods.
  • Refactor convert_type to extract type inference into a private _guess_type helper, and add :func:typing.overload signatures. :pr:3372

  • :class:Parameter typing improvements. :pr:2805

    • :class:Parameter is now an abstract base class, making explicit that it cannot be instantiated directly.
    • :attr:Parameter.name is now str instead of str | None. When expose_value=False, the name is set to "" instead of None.
    • The ctx parameter of :meth:Parameter.get_error_hint is now typed as Context | None, matching the runtime behavior.
  • Split string values from default_map for parameters with nargs > 1 or :class:Tuple type, matching environment variable behavior. :issue:2745 :pr:3364

  • Auto-detect type=UNPROCESSED for flag_value of non-basic types (not str, int, float, or bool), so programmer-provided Python objects like classes and enum members are passed through unchanged instead of being stringified. Previously type=click.UNPROCESSED had to be set explicitly. :issue:2012 :pr:3363

  • The error hint now uses :meth:Command.get_help_option_names to pick non-shadowed help option names, so Try '... -h' no longer points to a subcommand option that shadows -h. All surviving names are shown (-h/--help). :issue:2790 :pr:3208

  • Fix readline functionality on non-Windows platforms. Prompt text is now passed directly to readline instead of being printed separately, allowing proper backspace, line editing, and line wrapping behavior. :issue:2968 :pr:2969

  • Use :func:os.startfile on Windows to open URLs in :func:open_url, replacing the start built-in which cannot be invoked without shell=True. :issue:3164 :pr:3186

  • Fix Fish shell completion errors when option help text contains newlines. :issue:3043 :pr:3126

... (truncated)

Commits
  • 41f410f Release 8.4.0
  • e3e69e3 Add type annotations for instance attributes in utils (#3422)
  • 3bb230d WIP: Fix HelpFormatter.write_usage producing spurious characters (#3434)
  • 63274a7 click.get_pager_file: add tests (#1572 followup) (#3405)
  • 0551bf5 Fix HelpFormatter.write_usage producing spurious characters
  • fc41aa1 Apply class-body annotations to KeepOpenFile for consistency
  • b761eda Skip some tests on Windows
  • 98302ac Check PAGER usage, color preservation and edge-cases
  • dbdae17 Fix documentation
  • 1aa2d53 Redesigned tests and get_pager_file branching to be more clear and not set color
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=click&package-manager=pip&previous-version=8.3.3&new-version=8.4.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 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 +- requirements/lint.txt | 2 +- requirements/test-common.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index c3c02cccd85..8b3284bb84a 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -50,7 +50,7 @@ cfgv==3.5.0 # via pre-commit charset-normalizer==3.4.7 # via requests -click==8.3.3 +click==8.4.0 # via # pip-tools # slotscheck diff --git a/requirements/dev.txt b/requirements/dev.txt index 48699e31765..402435e68f9 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -50,7 +50,7 @@ cfgv==3.5.0 # via pre-commit charset-normalizer==3.4.7 # via requests -click==8.3.3 +click==8.4.0 # via # pip-tools # slotscheck diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index ed58e6c876f..eb528786532 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -14,7 +14,7 @@ certifi==2026.4.22 # via requests charset-normalizer==3.4.7 # via requests -click==8.3.3 +click==8.4.0 # via towncrier docutils==0.21.2 # via sphinx diff --git a/requirements/doc.txt b/requirements/doc.txt index 660c70163e8..64ba54abc64 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -14,7 +14,7 @@ certifi==2026.4.22 # via requests charset-normalizer==3.4.7 # via requests -click==8.3.3 +click==8.4.0 # via towncrier docutils==0.21.2 # via sphinx diff --git a/requirements/lint.txt b/requirements/lint.txt index 47865d7d125..8b3d6525c90 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -22,7 +22,7 @@ cffi==2.0.0 # pycares cfgv==3.5.0 # via pre-commit -click==8.3.3 +click==8.4.0 # via slotscheck cryptography==48.0.0 # via trustme diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 2fa57883159..dc964130606 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -12,7 +12,7 @@ blockbuster==1.5.26 # via -r requirements/test-common.in cffi==2.0.0 # via cryptography -click==8.3.3 +click==8.4.0 # via wait-for-it coverage==7.14.0 # via diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 27b43324777..95d7e45450b 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -28,7 +28,7 @@ cffi==2.0.0 # via # cryptography # pycares -click==8.3.3 +click==8.4.0 # via wait-for-it coverage==7.14.0 # via diff --git a/requirements/test.txt b/requirements/test.txt index ee214f01243..3f1cfc91577 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -28,7 +28,7 @@ cffi==2.0.0 # via # cryptography # pycares -click==8.3.3 +click==8.4.0 # via wait-for-it coverage==7.14.0 # via From 446f330a6f99eef650742ac422d4214cd82f0811 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 20:23:11 +0000 Subject: [PATCH 124/191] Bump aiodns from 4.0.0 to 4.0.3 (#12635) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [//]: # (dependabot-start) ⚠️ **Dependabot is rebasing this PR** ⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Bumps [aiodns](https://github.com/aio-libs/aiodns) from 4.0.0 to 4.0.3.
Changelog

Sourced from aiodns's changelog.

4.0.3

  • Restore license metadata that was dropped during the pyproject.toml migration in #244, so packaging tools again detect aiodns as MIT-licensed (#250).

4.0.2

  • Re-release of 4.0.1; the 4.0.1 wheel build failed because the release workflow still invoked python setup.py after #244 removed setup.py, so 4.0.1 never reached PyPI. The release workflow now uses python -m build (#248).

4.0.1

  • Fix Future exception was never retrieved when pycares raises AresError synchronously, e.g. for malformed hostnames (#245, fixes #231)
  • Modernized package setup using pyproject.toml instead of setup.py (#244)
  • Updated dependencies
    • Bumped mypy from 1.19.1 to 2.1.0 (#236, #239, #241, #242)
    • Bumped pytest from 9.0.2 to 9.0.3 (#237)
    • Bumped pytest-cov from 7.0.0 to 7.1.0 (#232)
    • Bumped dependabot/fetch-metadata from 2.4.0 to 3.1.0 (#227, #234, #240)
    • Bumped actions/download-artifact from 7.0.0 to 8.0.1 (#228, #235)
    • Bumped actions/upload-artifact from 6 to 7 (#229)
    • Bumped codecov/codecov-action from 5 to 6 (#233)
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=aiodns&package-manager=pip&previous-version=4.0.0&new-version=4.0.3)](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/base-ft.txt | 2 +- requirements/base.txt | 2 +- requirements/constraints.txt | 2 +- requirements/dev.txt | 2 +- requirements/lint.txt | 2 +- requirements/runtime-deps.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt index 10fa8f02117..972f93541f8 100644 --- a/requirements/base-ft.txt +++ b/requirements/base-ft.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/base-ft.txt --strip-extras requirements/base-ft.in # -aiodns==4.0.0 +aiodns==4.0.3 # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.1 # via -r requirements/runtime-deps.in diff --git a/requirements/base.txt b/requirements/base.txt index c1abe470372..e0edceac79c 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/base.txt --strip-extras requirements/base.in # -aiodns==4.0.0 +aiodns==4.0.3 # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.1 # via -r requirements/runtime-deps.in diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 8b3284bb84a..184499ce80e 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/constraints.txt --strip-extras requirements/constraints.in # -aiodns==4.0.0 +aiodns==4.0.3 # via # -r requirements/lint.in # -r requirements/runtime-deps.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 402435e68f9..0ecd8ce83ae 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/dev.txt --strip-extras requirements/dev.in # -aiodns==4.0.0 +aiodns==4.0.3 # via # -r requirements/lint.in # -r requirements/runtime-deps.in diff --git a/requirements/lint.txt b/requirements/lint.txt index 8b3d6525c90..ea061166130 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/lint.txt --resolver=backtracking --strip-extras requirements/lint.in # -aiodns==4.0.0 +aiodns==4.0.3 # via -r requirements/lint.in annotated-types==0.7.0 # via pydantic diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index df3530154c3..4bba60d46ed 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/runtime-deps.txt --strip-extras requirements/runtime-deps.in # -aiodns==4.0.0 +aiodns==4.0.3 # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.1 # via -r requirements/runtime-deps.in diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 95d7e45450b..abbf0178d64 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/test-ft.txt --strip-extras requirements/test-ft.in # -aiodns==4.0.0 +aiodns==4.0.3 # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.1 # via -r requirements/runtime-deps.in diff --git a/requirements/test.txt b/requirements/test.txt index 3f1cfc91577..396d0469999 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/test.txt --strip-extras requirements/test.in # -aiodns==4.0.0 +aiodns==4.0.3 # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.1 # via -r requirements/runtime-deps.in From a309741d8132e76548f4371a0c64bc662b8adddc Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 18 May 2026 23:35:13 +0100 Subject: [PATCH 125/191] Deprecate auth. Add encode_basic_auth. (#12499) (#12555) (cherry picked from commit 5cde279c1fc46d9b3396524a14d92f9c5bdeaf04) --------- Co-authored-by: J. Nick Koston --- CHANGES/12499.deprecation.rst | 7 ++++ CHANGES/12499.feature.rst | 3 ++ aiohttp/__init__.py | 3 +- aiohttp/client.py | 49 +++++++++++++++++++++++++ aiohttp/client_reqrep.py | 3 +- aiohttp/helpers.py | 42 ++++++++++++++++++---- docs/client_advanced.rst | 35 +++++++++++++----- docs/client_reference.rst | 26 +++++++++++++- tests/test_client_functional.py | 10 ++++++ tests/test_client_request.py | 18 +++++----- tests/test_client_session.py | 27 +++++++++++++- tests/test_helpers.py | 63 +++++++++++++++++++++++++-------- tests/test_proxy.py | 8 +++-- tests/test_proxy_functional.py | 10 ++++++ 14 files changed, 259 insertions(+), 45 deletions(-) create mode 100644 CHANGES/12499.deprecation.rst create mode 100644 CHANGES/12499.feature.rst diff --git a/CHANGES/12499.deprecation.rst b/CHANGES/12499.deprecation.rst new file mode 100644 index 00000000000..c9a5b533c77 --- /dev/null +++ b/CHANGES/12499.deprecation.rst @@ -0,0 +1,7 @@ +Deprecated :class:`~aiohttp.BasicAuth` and the ``auth`` / ``proxy_auth`` +parameters. They will be removed in aiohttp 4.0. Use the new +:func:`~aiohttp.encode_basic_auth` helper together with +``headers={"Authorization": ...}`` (or +``proxy_headers={"Proxy-Authorization": ...}`` for proxies) instead. +Note that ``encode_basic_auth()`` defaults to `utf-8`, not `latin1` +-- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12499.feature.rst b/CHANGES/12499.feature.rst new file mode 100644 index 00000000000..8b242432367 --- /dev/null +++ b/CHANGES/12499.feature.rst @@ -0,0 +1,3 @@ +Added :func:`~aiohttp.encode_basic_auth` for encoding HTTP Basic +Authentication credentials. Replaces the now-deprecated +:class:`~aiohttp.BasicAuth` -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/__init__.py b/aiohttp/__init__.py index c298b51424d..4bbf8d308ed 100644 --- a/aiohttp/__init__.py +++ b/aiohttp/__init__.py @@ -56,7 +56,7 @@ ) from .cookiejar import CookieJar as CookieJar, DummyCookieJar as DummyCookieJar from .formdata import FormData as FormData -from .helpers import BasicAuth, ChainMapProxy, ETag +from .helpers import BasicAuth, ChainMapProxy, ETag, encode_basic_auth from .http import ( HttpVersion as HttpVersion, HttpVersion10 as HttpVersion10, @@ -190,6 +190,7 @@ "ChainMapProxy", "DigestAuthMiddleware", "ETag", + "encode_basic_auth", "set_zlib_backend", # http "HttpVersion", diff --git a/aiohttp/client.py b/aiohttp/client.py index 09aa4d154de..382679214d2 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -425,6 +425,22 @@ def __init__( if cookies: self._cookie_jar.update_cookies(cookies) + if auth is not None: + warnings.warn( + "The 'auth' parameter is deprecated and will be removed in v4;" + " pass headers={'Authorization': " + "aiohttp.encode_basic_auth(login, password)} instead", + DeprecationWarning, + stacklevel=2, + ) + if proxy_auth is not None: + warnings.warn( + "The 'proxy_auth' parameter is deprecated and will be removed in v4;" + " pass proxy_headers={'Proxy-Authorization': " + "aiohttp.encode_basic_auth(login, password)} instead", + DeprecationWarning, + stacklevel=2, + ) self._connector = connector self._connector_owner = connector_owner self._default_auth = auth @@ -566,6 +582,23 @@ async def _request( ssl = _merge_ssl_params(ssl, verify_ssl, ssl_context, fingerprint) + if auth is not None: + warnings.warn( + "The 'auth' parameter is deprecated and will be removed in v4;" + " pass headers={'Authorization': " + "aiohttp.encode_basic_auth(login, password)} instead", + DeprecationWarning, + stacklevel=3, + ) + if proxy_auth is not None: + warnings.warn( + "The 'proxy_auth' parameter is deprecated and will be removed in v4;" + " pass proxy_headers={'Proxy-Authorization': " + "aiohttp.encode_basic_auth(login, password)} instead", + DeprecationWarning, + stacklevel=3, + ) + if data is not None and json is not None: raise ValueError( "data and json parameters can not be used at the same time" @@ -1131,6 +1164,22 @@ async def _ws_connect( max_msg_size: int = 4 * 1024 * 1024, decode_text: bool = True, ) -> "ClientWebSocketResponse[bool]": + if auth is not None: + warnings.warn( + "The 'auth' parameter is deprecated and will be removed in v4;" + " pass headers={'Authorization': " + "aiohttp.encode_basic_auth(login, password)} instead", + DeprecationWarning, + stacklevel=3, + ) + if proxy_auth is not None: + warnings.warn( + "The 'proxy_auth' parameter is deprecated and will be removed in v4;" + " pass proxy_headers={'Proxy-Authorization': " + "aiohttp.encode_basic_auth(login, password)} instead", + DeprecationWarning, + stacklevel=3, + ) if timeout is not sentinel: if isinstance(timeout, ClientWSTimeout): ws_timeout = timeout diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index 92b3529bf13..0353ec34f03 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -40,6 +40,7 @@ BasicAuth, HeadersMixin, TimerNoop, + _basic_auth_no_warn, noop, reify, sentinel, @@ -1019,7 +1020,7 @@ def update_host(self, url: URL) -> None: # basic auth info if url.raw_user or url.raw_password: - self.auth = helpers.BasicAuth(url.user or "", url.password or "") + self.auth = _basic_auth_no_warn(url.user or "", url.password or "") def update_version(self, version: http.HttpVersion | str) -> None: """Convert request version to two elements tuple. diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index ebc73e19205..5988aab4aa8 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -14,6 +14,7 @@ import re import sys import time +import warnings import weakref from collections import namedtuple from collections.abc import Callable, Generator, Iterable, Iterator, Mapping @@ -115,6 +116,18 @@ def __await__(self) -> Generator[None, None, None]: yield +def encode_basic_auth(login: str, password: str = "", encoding: str = "utf-8") -> str: + """Encode HTTP Basic Authentication credentials as an Authorization header value. + + Returns a string of the form ``"Basic "`` suitable for use as the + value of the ``Authorization`` (or ``Proxy-Authorization``) header. + """ + if ":" in login: + raise ValueError('A ":" is not allowed in login (RFC 7617#section-2)') + creds = f"{login}:{password}".encode(encoding) + return "Basic " + base64.b64encode(creds).decode(encoding) + + class BasicAuth(namedtuple("BasicAuth", ["login", "password", "encoding"])): """Http basic authentication helper.""" @@ -130,6 +143,13 @@ def __new__( if ":" in login: raise ValueError('A ":" is not allowed in login (RFC 1945#section-11.1)') + warnings.warn( + "BasicAuth is deprecated and will be removed in aiohttp 4.0; " + "use aiohttp.encode_basic_auth() with " + "headers={'Authorization': ...} instead", + DeprecationWarning, + stacklevel=2, + ) return super().__new__(cls, login, password, encoding) @classmethod @@ -159,7 +179,7 @@ def decode(cls, auth_header: str, encoding: str = "latin1") -> "BasicAuth": except ValueError: raise ValueError("Invalid credentials.") - return cls(username, password, encoding=encoding) + return _basic_auth_no_warn(username, password, encoding) @classmethod def from_url(cls, url: URL, *, encoding: str = "latin1") -> Optional["BasicAuth"]: @@ -170,12 +190,22 @@ def from_url(cls, url: URL, *, encoding: str = "latin1") -> Optional["BasicAuth" # to already have these values parsed from the netloc in the cache. if url.raw_user is None and url.raw_password is None: return None - return cls(url.user or "", url.password or "", encoding=encoding) + return _basic_auth_no_warn(url.user or "", url.password or "", encoding) def encode(self) -> str: """Encode credentials.""" - creds = (f"{self.login}:{self.password}").encode(self.encoding) - return "Basic %s" % base64.b64encode(creds).decode(self.encoding) + return encode_basic_auth(self.login, self.password, self.encoding) + + +def _basic_auth_no_warn( + login: str, password: str = "", encoding: str = "latin1" +) -> BasicAuth: + """Construct a BasicAuth without emitting the deprecation warning. + + For internal use only. Bypasses BasicAuth.__new__ so that aiohttp's own + machinery doesn't trigger deprecation warnings in user code. + """ + return tuple.__new__(BasicAuth, (login, password, encoding)) def strip_auth_from_url(url: URL) -> tuple[URL, BasicAuth | None]: @@ -184,7 +214,7 @@ def strip_auth_from_url(url: URL) -> tuple[URL, BasicAuth | None]: # to already have these values parsed from the netloc in the cache. if url.raw_user is None and url.raw_password is None: return url, None - return url.with_user(None), BasicAuth(url.user or "", url.password or "") + return url.with_user(None), _basic_auth_no_warn(url.user or "", url.password or "") def netrc_from_env() -> netrc.netrc | None: @@ -262,7 +292,7 @@ def basicauth_from_netrc(netrc_obj: netrc.netrc | None, host: str) -> BasicAuth: if password is None: password = "" - return BasicAuth(username, password) + return _basic_auth_no_warn(username, password) def proxies_from_env() -> dict[str, ProxyInfo]: diff --git a/docs/client_advanced.rst b/docs/client_advanced.rst index 18c274ca7f5..e4a94f50942 100644 --- a/docs/client_advanced.rst +++ b/docs/client_advanced.rst @@ -59,14 +59,21 @@ For *text/plain* :: Authentication -------------- -Instead of setting the ``Authorization`` header directly, -:class:`ClientSession` and individual request methods provide an ``auth`` -argument. An instance of :class:`BasicAuth` can be passed in like this:: +For HTTP Basic Authentication, build the ``Authorization`` header using +:func:`encode_basic_auth` and pass it via ``headers``:: - auth = BasicAuth(login="...", password="...") - async with ClientSession(auth=auth) as session: + from aiohttp import ClientSession, encode_basic_auth + + headers = {"Authorization": encode_basic_auth("user", "pass")} + async with ClientSession(headers=headers) as session: ... +.. deprecated:: 3.14 + + The ``auth`` parameter and the :class:`BasicAuth` class are deprecated and + will be removed in 4.0. Use :func:`encode_basic_auth` together with the + ``headers`` parameter as shown above. + For HTTP digest authentication, use the :class:`DigestAuthMiddleware` client middleware:: from aiohttp import ClientSession, DigestAuthMiddleware @@ -722,11 +729,13 @@ To connect, use the *proxy* parameter:: It also supports proxy authorization:: + from aiohttp import encode_basic_auth + async with aiohttp.ClientSession() as session: - proxy_auth = aiohttp.BasicAuth('user', 'pass') + proxy_headers = {"Proxy-Authorization": encode_basic_auth("user", "pass")} async with session.get("http://python.org", proxy="http://proxy.com", - proxy_auth=proxy_auth) as resp: + proxy_headers=proxy_headers) as resp: print(resp.status) Authentication credentials can be passed in proxy URL:: @@ -736,11 +745,19 @@ Authentication credentials can be passed in proxy URL:: And you may set default proxy:: - proxy_auth = aiohttp.BasicAuth('user', 'pass') - async with aiohttp.ClientSession(proxy="http://proxy.com", proxy_auth=proxy_auth) as session: + proxy_headers = {"Proxy-Authorization": encode_basic_auth("user", "pass")} + async with aiohttp.ClientSession( + proxy="http://proxy.com", proxy_headers=proxy_headers + ) as session: async with session.get("http://python.org") as resp: print(resp.status) +.. deprecated:: 3.14 + + The ``proxy_auth`` parameter is deprecated and will be removed in 4.0. Use + :func:`encode_basic_auth` with ``proxy_headers={"Proxy-Authorization": ...}`` + as shown above. + Contrary to the ``requests`` library, it won't read environment variables by default. But you can do so by passing ``trust_env=True`` into :class:`aiohttp.ClientSession` diff --git a/docs/client_reference.rst b/docs/client_reference.rst index 651190c3cdf..f9f3dfcfef8 100644 --- a/docs/client_reference.rst +++ b/docs/client_reference.rst @@ -2327,6 +2327,22 @@ Utilities +.. function:: encode_basic_auth(login, password='', encoding='utf-8') + + Encode HTTP Basic Authentication credentials as a value suitable for the + ``Authorization`` (or ``Proxy-Authorization``) header:: + + headers = {"Authorization": encode_basic_auth("user", "pass")} + + :param str login: login + :param str password: password (``''`` by default) + :param str encoding: encoding (``'utf-8'`` by default) + :return: a string of the form ``"Basic "`` + :rtype: str + + .. versionadded:: 3.14 + + .. class:: BasicAuth(login, password='', encoding='latin1') HTTP basic authentication helper. @@ -2336,9 +2352,17 @@ Utilities :param str encoding: encoding (``'latin1'`` by default) - Should be used for specifying authorization data in client API, + Previously this was used for specifying authorization data in client API, e.g. *auth* parameter for :meth:`ClientSession.request() `. + .. deprecated:: 3.14 + + Constructing :class:`BasicAuth` is deprecated and will be removed in + 4.0. Use :func:`encode_basic_auth` together with the ``headers`` + parameter (or ``proxy_headers`` for proxies) instead. The + :meth:`decode` and :meth:`from_url` class methods remain available for + parsing. + .. classmethod:: decode(auth_header, encoding='latin1') diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 802b8ee239d..3853f88dd9e 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -66,6 +66,16 @@ from aiohttp.test_utils import TestClient, TestServer, unused_port from aiohttp.typedefs import Handler +pytestmark = [ + pytest.mark.filterwarnings(r"ignore:BasicAuth is deprecated:DeprecationWarning"), + pytest.mark.filterwarnings( + r"ignore:The 'auth' parameter is deprecated:DeprecationWarning" + ), + pytest.mark.filterwarnings( + r"ignore:The 'proxy_auth' parameter is deprecated:DeprecationWarning" + ), +] + @pytest.fixture(autouse=True) def cleanup( diff --git a/tests/test_client_request.py b/tests/test_client_request.py index 5e23f085d45..5f5cf4a849f 100644 --- a/tests/test_client_request.py +++ b/tests/test_client_request.py @@ -412,17 +412,17 @@ def test_ipv6_nondefault_https_port(make_request) -> None: def test_basic_auth(make_request) -> None: - req = make_request( - "get", "http://python.org", auth=aiohttp.BasicAuth("nkim", "1234") - ) + with pytest.warns(DeprecationWarning, match="BasicAuth is deprecated"): + auth = aiohttp.BasicAuth("nkim", "1234") + req = make_request("get", "http://python.org", auth=auth) assert "AUTHORIZATION" in req.headers assert "Basic bmtpbToxMjM0" == req.headers["AUTHORIZATION"] def test_basic_auth_utf8(make_request) -> None: - req = make_request( - "get", "http://python.org", auth=aiohttp.BasicAuth("nkim", "секрет", "utf-8") - ) + with pytest.warns(DeprecationWarning, match="BasicAuth is deprecated"): + auth = aiohttp.BasicAuth("nkim", "секрет", "utf-8") + req = make_request("get", "http://python.org", auth=auth) assert "AUTHORIZATION" in req.headers assert "Basic bmtpbTrRgdC10LrRgNC10YI=" == req.headers["AUTHORIZATION"] @@ -447,9 +447,9 @@ def test_basic_auth_no_user_from_url(make_request) -> None: def test_basic_auth_from_url_overridden(make_request) -> None: - req = make_request( - "get", "http://garbage@python.org", auth=aiohttp.BasicAuth("nkim", "1234") - ) + with pytest.warns(DeprecationWarning, match="BasicAuth is deprecated"): + auth = aiohttp.BasicAuth("nkim", "1234") + req = make_request("get", "http://garbage@python.org", auth=auth) assert "AUTHORIZATION" in req.headers assert "Basic bmtpbToxMjM0" == req.headers["AUTHORIZATION"] assert "python.org" == req.host diff --git a/tests/test_client_session.py b/tests/test_client_session.py index c316170d4a0..1060d1f2516 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -882,7 +882,10 @@ def test_proxy_str(session, params) -> None: ] -async def test_default_proxy(loop: asyncio.AbstractEventLoop) -> None: +@pytest.mark.filterwarnings( + r"ignore:The 'proxy_auth' parameter is deprecated:DeprecationWarning" +) +async def test_default_proxy() -> None: proxy_url = URL("http://proxy.example.com") proxy_auth = mock.Mock() proxy_url2 = URL("http://proxy.example2.com") @@ -1414,6 +1417,10 @@ async def test_netrc_auth_from_home_directory(auth_server: TestServer) -> None: @pytest.mark.usefixtures("netrc_default_contents") +@pytest.mark.filterwarnings( + r"ignore:The 'auth' parameter is deprecated:DeprecationWarning", + r"ignore:BasicAuth is deprecated:DeprecationWarning", +) async def test_netrc_auth_overridden_by_explicit_auth(auth_server: TestServer) -> None: """Test that explicit auth parameter overrides netrc authentication.""" async with ( @@ -1428,6 +1435,24 @@ async def test_netrc_auth_overridden_by_explicit_auth(auth_server: TestServer) - assert text == "auth:Basic ZXhwbGljaXRfdXNlcjpleHBsaWNpdF9wYXNz" +async def test_client_session_auth_deprecated() -> None: + """ClientSession(auth=...) emits a DeprecationWarning.""" + with pytest.warns(DeprecationWarning, match="'auth' parameter is deprecated"): + session = ClientSession( + auth=aiohttp.helpers._basic_auth_no_warn("user", "pass") + ) + await session.close() + + +async def test_client_session_proxy_auth_deprecated() -> None: + """ClientSession(proxy_auth=...) emits a DeprecationWarning.""" + with pytest.warns(DeprecationWarning, match="'proxy_auth' parameter is deprecated"): + session = ClientSession( + proxy_auth=aiohttp.helpers._basic_auth_no_warn("user", "pass") + ) + await session.close() + + @pytest.mark.usefixtures("netrc_other_host") async def test_netrc_auth_host_not_in_netrc(auth_server: TestServer) -> None: """Test that netrc lookup returns None when host is not in netrc file.""" diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 4baf6b8c0cd..3ddf79c7666 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -3,6 +3,7 @@ import datetime import gc import sys +import warnings import weakref from math import ceil, modf from pathlib import Path @@ -128,18 +129,56 @@ def test_basic_with_auth_colon_in_login() -> None: def test_basic_auth3() -> None: - auth = helpers.BasicAuth("nkim") + with pytest.warns(DeprecationWarning, match="BasicAuth is deprecated"): + auth = helpers.BasicAuth("nkim") assert auth.login == "nkim" assert auth.password == "" def test_basic_auth4() -> None: - auth = helpers.BasicAuth("nkim", "pwd") + with pytest.warns(DeprecationWarning, match="BasicAuth is deprecated"): + auth = helpers.BasicAuth("nkim", "pwd") assert auth.login == "nkim" assert auth.password == "pwd" assert auth.encode() == "Basic bmtpbTpwd2Q=" +def test_basic_auth_deprecated() -> None: + with pytest.warns( + DeprecationWarning, + match=( + "BasicAuth is deprecated and will be removed in aiohttp 4.0; " + "use aiohttp.encode_basic_auth" + ), + ): + helpers.BasicAuth("user", "pass") + + +def test_encode_basic_auth() -> None: + assert helpers.encode_basic_auth("nkim", "pwd") == "Basic bmtpbTpwd2Q=" + assert helpers.encode_basic_auth("") == "Basic Og==" + assert ( + helpers.encode_basic_auth("usér", "pàss", encoding="utf-8") + == "Basic dXPDqXI6cMOgc3M=" + ) + + +def test_encode_basic_auth_rejects_colon_in_login() -> None: + with pytest.raises(ValueError): + helpers.encode_basic_auth("user:1", "pwd") + + +def test_basic_auth_no_warn_helpers_silent() -> None: + """Internal aiohttp paths must not raise BasicAuth's deprecation warning.""" + with warnings.catch_warnings(): + warnings.simplefilter("error", DeprecationWarning) + url = URL("http://user:pass@example.com/") + helpers.strip_auth_from_url(url) + helpers.BasicAuth.decode("Basic dXNlcjpwYXNz") + helpers.BasicAuth.from_url(url) + helpers._basic_auth_no_warn("user", "pass") + + @pytest.mark.parametrize( "header", ( @@ -183,18 +222,12 @@ def test_basic_auth_decode_invalid_credentials() -> None: @pytest.mark.parametrize( "credentials, expected_auth", ( - (":", helpers.BasicAuth(login="", password="", encoding="latin1")), - ( - "username:", - helpers.BasicAuth(login="username", password="", encoding="latin1"), - ), - ( - ":password", - helpers.BasicAuth(login="", password="password", encoding="latin1"), - ), + (":", helpers._basic_auth_no_warn("", "", "latin1")), + ("username:", helpers._basic_auth_no_warn("username", "", "latin1")), + (":password", helpers._basic_auth_no_warn("", "password", "latin1")), ( "username:password", - helpers.BasicAuth(login="username", password="password", encoding="latin1"), + helpers._basic_auth_no_warn("username", "password", "latin1"), ), ), ) @@ -878,15 +911,15 @@ def test_netrc_from_home_does_not_raise_if_access_denied( [ ( "machine example.com login username password pass\n", - helpers.BasicAuth("username", "pass"), + helpers._basic_auth_no_warn("username", "pass", "latin1"), ), ( "machine example.com account username password pass\n", - helpers.BasicAuth("username", "pass"), + helpers._basic_auth_no_warn("username", "pass", "latin1"), ), ( "machine example.com password pass\n", - helpers.BasicAuth("", "pass"), + helpers._basic_auth_no_warn("", "pass", "latin1"), ), ], indirect=("netrc_contents",), diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 4c506cc5730..5599bf804f4 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -859,11 +859,13 @@ async def make_conn(): self.assertEqual(req.url, URL("http://localhost:1234/path")) def test_proxy_auth_property(self) -> None: + with pytest.warns(DeprecationWarning, match="BasicAuth is deprecated"): + proxy_auth = aiohttp.helpers.BasicAuth("user", "pass") req = aiohttp.ClientRequest( "GET", URL("http://localhost:1234/path"), proxy=URL("http://proxy.example.com"), - proxy_auth=aiohttp.helpers.BasicAuth("user", "pass"), + proxy_auth=proxy_auth, loop=self.loop, ) self.assertEqual(("user", "pass", "latin1"), req.proxy_auth) @@ -959,10 +961,12 @@ async def make_conn(): spec_set=True, ) def test_https_auth(self, start_connection: Any, ClientRequestMock: Any) -> None: + with pytest.warns(DeprecationWarning, match="BasicAuth is deprecated"): + auth = aiohttp.helpers.BasicAuth("user", "pass") proxy_req = ClientRequest( "GET", URL("http://proxy.example.com"), - auth=aiohttp.helpers.BasicAuth("user", "pass"), + auth=auth, loop=self.loop, ) ClientRequestMock.return_value = proxy_req diff --git a/tests/test_proxy_functional.py b/tests/test_proxy_functional.py index bf6ecc9a933..a240692d0d8 100644 --- a/tests/test_proxy_functional.py +++ b/tests/test_proxy_functional.py @@ -23,6 +23,16 @@ ASYNCIO_SUPPORTS_TLS_IN_TLS = sys.version_info >= (3, 11) +pytestmark = [ + pytest.mark.filterwarnings(r"ignore:BasicAuth is deprecated:DeprecationWarning"), + pytest.mark.filterwarnings( + r"ignore:The 'auth' parameter is deprecated:DeprecationWarning" + ), + pytest.mark.filterwarnings( + r"ignore:The 'proxy_auth' parameter is deprecated:DeprecationWarning" + ), +] + @pytest.fixture def secure_proxy_url(tls_certificate_pem_path): From fd037ee27d2e393940b7cf1b73bca834a7d92e84 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 23:01:35 +0000 Subject: [PATCH 126/191] [PR #12629/66cc7b68 backport][3.14] Install Python via astral-sh/setup-uv in test/autobahn jobs (#12637) Co-authored-by: J. Nick Koston --- .github/workflows/ci-cd.yml | 52 +++++++++++++------------------------ CHANGES/12629.contrib.rst | 7 +++++ 2 files changed, 25 insertions(+), 34 deletions(-) create mode 100644 CHANGES/12629.contrib.rst diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index eea8db6665e..27708170b49 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -175,30 +175,22 @@ jobs: submodules: true - name: Setup Python ${{ matrix.pyver }} id: python-install - uses: actions/setup-python@v6 + # important: do not use system python + env: + UV_PYTHON_PREFERENCE: only-managed + uses: astral-sh/setup-uv@v8.1.0 with: - allow-prereleases: true python-version: ${{ matrix.pyver }} - - name: Get pip cache dir - id: pip-cache - run: | - echo "dir=$(pip cache dir)" >> "${GITHUB_OUTPUT}" - shell: bash - - name: Cache PyPI - uses: actions/cache@v5.0.5 - with: - key: pip-ci-${{ runner.os }}-${{ matrix.pyver }}-${{ matrix.no-extensions }}-${{ hashFiles('requirements/*.txt') }} - path: ${{ steps.pip-cache.outputs.dir }} - restore-keys: | - pip-ci-${{ runner.os }}-${{ matrix.pyver }}-${{ matrix.no-extensions }}- + activate-environment: true + enable-cache: true - name: Update pip, wheel, setuptools, build, twine run: | - python -m pip install -U pip wheel setuptools build twine + uv pip install -U pip wheel setuptools build twine - name: Install dependencies env: DEPENDENCY_GROUP: test${{ endsWith(matrix.pyver, 't') && '-ft' || '' }} run: | - python -Im pip install -r requirements/${{ env.DEPENDENCY_GROUP }}.in -c requirements/${{ env.DEPENDENCY_GROUP }}.txt + uv pip install -r requirements/${{ env.DEPENDENCY_GROUP }}.in -c requirements/${{ env.DEPENDENCY_GROUP }}.txt - name: Set PYTHON_GIL=0 for free-threading builds if: ${{ endsWith(matrix.pyver, 't') }} run: echo "PYTHON_GIL=0" >> $GITHUB_ENV @@ -215,7 +207,7 @@ jobs: - name: Install self env: AIOHTTP_NO_EXTENSIONS: ${{ matrix.no-extensions }} - run: python -m pip install -e . + run: uv pip install -e . - name: Run unittests env: COLOR: yes @@ -290,30 +282,22 @@ jobs: submodules: true - name: Setup Python ${{ matrix.pyver }} id: python-install - uses: actions/setup-python@v6 + # important: do not use system python + env: + UV_PYTHON_PREFERENCE: only-managed + uses: astral-sh/setup-uv@v8.1.0 with: - allow-prereleases: true python-version: ${{ matrix.pyver }} - - name: Get pip cache dir - id: pip-cache - run: | - echo "dir=$(pip cache dir)" >> "${GITHUB_OUTPUT}" - shell: bash - - name: Cache PyPI - uses: actions/cache@v5.0.5 - with: - key: pip-ci-${{ runner.os }}-${{ matrix.pyver }}-${{ matrix.no-extensions }}-${{ hashFiles('requirements/*.txt') }} - path: ${{ steps.pip-cache.outputs.dir }} - restore-keys: | - pip-ci-${{ runner.os }}-${{ matrix.pyver }}-${{ matrix.no-extensions }}- + activate-environment: true + enable-cache: true - name: Update pip, wheel, setuptools, build, twine run: | - python -m pip install -U pip wheel setuptools build twine + uv pip install -U pip wheel setuptools build twine - name: Install dependencies env: DEPENDENCY_GROUP: test${{ endsWith(matrix.pyver, 't') && '-ft' || '' }} run: | - python -Im pip install -r requirements/${{ env.DEPENDENCY_GROUP }}.in -c requirements/${{ env.DEPENDENCY_GROUP }}.txt + uv pip install -r requirements/${{ env.DEPENDENCY_GROUP }}.in -c requirements/${{ env.DEPENDENCY_GROUP }}.txt - name: Restore llhttp generated files if: ${{ matrix.no-extensions == '' }} uses: actions/download-artifact@v8 @@ -327,7 +311,7 @@ jobs: - name: Install self env: AIOHTTP_NO_EXTENSIONS: ${{ matrix.no-extensions }} - run: python -m pip install -e . + run: uv pip install -e . - name: Run unittests env: COLOR: yes diff --git a/CHANGES/12629.contrib.rst b/CHANGES/12629.contrib.rst new file mode 100644 index 00000000000..8c10b198425 --- /dev/null +++ b/CHANGES/12629.contrib.rst @@ -0,0 +1,7 @@ +Switched the CI ``test`` and ``autobahn`` jobs from +``actions/setup-python`` to ``astral-sh/setup-uv`` for installing +interpreters, cutting the ``Setup Python`` step from 40-58s to a +few seconds on ``macos-latest`` and ``windows-latest`` runners for +variants not in the hosted tool-cache (notably the free-threaded +``3.14t``) +-- by :user:`bdraco`. From f54c40851b0d6c4bbdab97ba518a223adda32478 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 19 May 2026 01:23:00 +0100 Subject: [PATCH 127/191] Drop cookies on redirect (#12550) (#12640) (cherry picked from commit d57efb05f5073071ceb2d3b35d72d9d0bc4512a2) --- CHANGES/12540.bugfix.rst | 1 + aiohttp/client.py | 1 + tests/test_client_functional.py | 16 ++++++++++++++-- 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 CHANGES/12540.bugfix.rst diff --git a/CHANGES/12540.bugfix.rst b/CHANGES/12540.bugfix.rst new file mode 100644 index 00000000000..dfd98129e4b --- /dev/null +++ b/CHANGES/12540.bugfix.rst @@ -0,0 +1 @@ +Fixed per-request ``cookies`` not being dropped on cross-origin redirects -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/client.py b/aiohttp/client.py index 382679214d2..d9d8dfd5db7 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -971,6 +971,7 @@ async def _connect_and_send_request( if url.origin() != redirect_origin: auth = None + cookies = None headers.pop(hdrs.AUTHORIZATION, None) headers.pop(hdrs.COOKIE, None) headers.pop(hdrs.PROXY_AUTHORIZATION, None) diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 3853f88dd9e..66b86eb64ac 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -3593,8 +3593,20 @@ async def close(self) -> None: async with aiohttp.ClientSession( connector=connector, auth=aiohttp.BasicAuth("user", "pass") ) as client: - resp = await client.get(url_from) - assert resp.status == 200 + async with client.get( + url_from, + headers={ + "Proxy-Authorization": "Basic dXNlcjpwYXNz", + "Cookie": "a=b", + }, + ) as resp: + assert resp.status == 200 + async with client.get( + url_from, + headers={"Proxy-Authorization": "Basic dXNlcjpwYXNz"}, + cookies={"a": "b"}, + ) as resp: + assert resp.status == 200 async def test_drop_auth_on_redirect_to_other_host_with_global_auth_and_base_url( From 63ff485578f051f92fadf4e6ea8563bd7add8d7c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 May 2026 19:40:30 -0700 Subject: [PATCH 128/191] [PR #12641/1aef5d70 backport][3.13] Make pip command configurable via PIP variable in Makefile (#12644) --- CHANGES/12641.contrib.rst | 5 +++++ Makefile | 11 ++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 CHANGES/12641.contrib.rst diff --git a/CHANGES/12641.contrib.rst b/CHANGES/12641.contrib.rst new file mode 100644 index 00000000000..f431e0fd1e3 --- /dev/null +++ b/CHANGES/12641.contrib.rst @@ -0,0 +1,5 @@ +Made the ``pip`` command used by the :file:`Makefile` configurable via a +``PIP`` variable; downstream consumers can now run, for example, +``make .develop PIP="uv pip"`` to install via ``uv`` without us +maintaining a parallel target +-- by :user:`bdraco`. diff --git a/Makefile b/Makefile index 893565cc4e0..27db4fe9327 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ to-hash-one = $(dir $1).hash/$(addsuffix .hash,$(notdir $1)) to-hash = $(foreach fname,$1,$(call to-hash-one,$(fname))) +PIP ?= python -m pip CYS := $(wildcard aiohttp/*.pyx) $(wildcard aiohttp/*.pyi) $(wildcard aiohttp/*.pxd) $(wildcard aiohttp/_websocket/*.pyx) $(wildcard aiohttp/_websocket/*.pyi) $(wildcard aiohttp/_websocket/*.pxd) PYXS := $(wildcard aiohttp/*.pyx) $(wildcard aiohttp/_websocket/*.pyx) CS := $(wildcard aiohttp/*.c) $(wildcard aiohttp/_websocket/*.c) @@ -47,10 +48,10 @@ endif .SECONDARY: $(call to-hash,$(ALLS)) .update-pip: - @python -m pip install --upgrade pip + @$(PIP) install --upgrade pip .install-cython: .update-pip $(call to-hash,requirements/cython.txt) - @python -m pip install -r requirements/cython.in -c requirements/cython.txt + @$(PIP) install -r requirements/cython.in -c requirements/cython.txt @touch .install-cython aiohttp/_find_header.c: $(call to-hash,aiohttp/hdrs.py ./tools/gen.py) @@ -85,7 +86,7 @@ cythonize: .install-cython $(PYXS:.pyx=.c) aiohttp/_websocket/reader_c.c cythonize-nodeps: $(PYXS:.pyx=.c) aiohttp/_websocket/reader_c.c .install-deps: .install-cython $(PYXS:.pyx=.c) aiohttp/_websocket/reader_c.c $(call to-hash,$(CYS) $(REQS)) - @python -m pip install -r requirements/dev.in -c requirements/dev.txt + @$(PIP) install -r requirements/dev.in -c requirements/dev.txt @touch .install-deps .PHONY: lint @@ -100,7 +101,7 @@ mypy: mypy .develop: .install-deps generate-llhttp $(call to-hash,$(PYS) $(CYS) $(CS)) - python -m pip install -e . -c requirements/runtime-deps.txt + $(PIP) install -e . -c requirements/runtime-deps.txt @touch .develop .PHONY: test @@ -182,7 +183,7 @@ doc-spelling: .PHONY: install install: .update-pip - @python -m pip install -r requirements/dev.in -c requirements/dev.txt + @$(PIP) install -r requirements/dev.in -c requirements/dev.txt .PHONY: install-dev install-dev: .develop From 7978c715c3a6d0a8bc5759b53bab1923355f84b9 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 02:50:29 +0000 Subject: [PATCH 129/191] [PR #12641/1aef5d70 backport][3.14] Make pip command configurable via PIP variable in Makefile (#12642) Co-authored-by: J. Nick Koston --- CHANGES/12641.contrib.rst | 5 +++++ Makefile | 11 ++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 CHANGES/12641.contrib.rst diff --git a/CHANGES/12641.contrib.rst b/CHANGES/12641.contrib.rst new file mode 100644 index 00000000000..f431e0fd1e3 --- /dev/null +++ b/CHANGES/12641.contrib.rst @@ -0,0 +1,5 @@ +Made the ``pip`` command used by the :file:`Makefile` configurable via a +``PIP`` variable; downstream consumers can now run, for example, +``make .develop PIP="uv pip"`` to install via ``uv`` without us +maintaining a parallel target +-- by :user:`bdraco`. diff --git a/Makefile b/Makefile index 1d85f8cdb23..502db01218e 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,7 @@ to-hash-one = $(dir $1).hash/$(addsuffix .hash,$(notdir $1)) to-hash = $(foreach fname,$1,$(call to-hash-one,$(fname))) CYTHON_EXTRA ?= +PIP ?= python -m pip CYS := $(wildcard aiohttp/*.pyx) $(wildcard aiohttp/*.pyi) $(wildcard aiohttp/*.pxd) $(wildcard aiohttp/_websocket/*.pyx) $(wildcard aiohttp/_websocket/*.pyi) $(wildcard aiohttp/_websocket/*.pxd) PYXS := $(wildcard aiohttp/*.pyx) $(wildcard aiohttp/_websocket/*.pyx) CS := $(wildcard aiohttp/*.c) $(wildcard aiohttp/_websocket/*.c) @@ -48,10 +49,10 @@ endif .SECONDARY: $(call to-hash,$(ALLS)) .update-pip: - @python -m pip install --upgrade pip + @$(PIP) install --upgrade pip .install-cython: .update-pip $(call to-hash,requirements/cython.txt) - @python -m pip install -r requirements/cython.in -c requirements/cython.txt + @$(PIP) install -r requirements/cython.in -c requirements/cython.txt @touch .install-cython aiohttp/_find_header.c: $(call to-hash,aiohttp/hdrs.py ./tools/gen.py) @@ -86,7 +87,7 @@ cythonize: .install-cython $(PYXS:.pyx=.c) aiohttp/_websocket/reader_c.c cythonize-nodeps: $(PYXS:.pyx=.c) aiohttp/_websocket/reader_c.c .install-deps: .install-cython $(PYXS:.pyx=.c) aiohttp/_websocket/reader_c.c $(call to-hash,$(CYS) $(REQS)) - @python -m pip install -r requirements/dev.in -c requirements/dev.txt + @$(PIP) install -r requirements/dev.in -c requirements/dev.txt @touch .install-deps .PHONY: lint @@ -101,7 +102,7 @@ mypy: mypy .develop: .install-deps generate-llhttp $(call to-hash,$(PYS) $(CYS) $(CS)) - python -m pip install -e . -c requirements/runtime-deps.txt + $(PIP) install -e . -c requirements/runtime-deps.txt @touch .develop .PHONY: test @@ -173,7 +174,7 @@ doc-spelling: .PHONY: install install: .update-pip - @python -m pip install -r requirements/dev.in -c requirements/dev.txt + @$(PIP) install -r requirements/dev.in -c requirements/dev.txt .PHONY: install-dev install-dev: .develop From e1ec3c563d2e59e958b6855bca816ec1126d5f11 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 03:11:59 +0000 Subject: [PATCH 130/191] [PR #12643/348f0a1e backport][3.14] Combine build-tool update and dependency install into one CI step per job (#12645) --- .github/workflows/ci-cd.yml | 39 +++++++++---------------------------- CHANGES/12643.contrib.rst | 5 +++++ 2 files changed, 14 insertions(+), 30 deletions(-) create mode 100644 CHANGES/12643.contrib.rst diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 27708170b49..c954482a635 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -77,12 +77,9 @@ jobs: path: ~/.cache/pip restore-keys: | pip-lint- - - name: Update pip, wheel, setuptools, build, twine - run: | - python -m pip install -U pip wheel setuptools build twine - name: Install dependencies run: | - python -m pip install -r requirements/lint.in -c requirements/lint.txt + python -m pip install -U pip wheel setuptools build twine -r requirements/lint.in -c requirements/lint.txt - name: Install self run: | python -m pip install . -c requirements/runtime-deps.txt @@ -183,14 +180,11 @@ jobs: python-version: ${{ matrix.pyver }} activate-environment: true enable-cache: true - - name: Update pip, wheel, setuptools, build, twine - run: | - uv pip install -U pip wheel setuptools build twine - name: Install dependencies env: DEPENDENCY_GROUP: test${{ endsWith(matrix.pyver, 't') && '-ft' || '' }} run: | - uv pip install -r requirements/${{ env.DEPENDENCY_GROUP }}.in -c requirements/${{ env.DEPENDENCY_GROUP }}.txt + uv pip install -U pip wheel setuptools build twine -r requirements/${{ env.DEPENDENCY_GROUP }}.in -c requirements/${{ env.DEPENDENCY_GROUP }}.txt - name: Set PYTHON_GIL=0 for free-threading builds if: ${{ endsWith(matrix.pyver, 't') }} run: echo "PYTHON_GIL=0" >> $GITHUB_ENV @@ -290,14 +284,11 @@ jobs: python-version: ${{ matrix.pyver }} activate-environment: true enable-cache: true - - name: Update pip, wheel, setuptools, build, twine - run: | - uv pip install -U pip wheel setuptools build twine - name: Install dependencies env: DEPENDENCY_GROUP: test${{ endsWith(matrix.pyver, 't') && '-ft' || '' }} run: | - uv pip install -r requirements/${{ env.DEPENDENCY_GROUP }}.in -c requirements/${{ env.DEPENDENCY_GROUP }}.txt + uv pip install -U pip wheel setuptools build twine -r requirements/${{ env.DEPENDENCY_GROUP }}.in -c requirements/${{ env.DEPENDENCY_GROUP }}.txt - name: Restore llhttp generated files if: ${{ matrix.no-extensions == '' }} uses: actions/download-artifact@v8 @@ -369,12 +360,9 @@ jobs: python-version: 3.13.2 cache: pip cache-dependency-path: requirements/*.txt - - name: Update pip, wheel, setuptools, build, twine - run: | - python -m pip install -U pip wheel setuptools build twine - name: Install dependencies run: | - python -m pip install -r requirements/test.in -c requirements/test.txt + python -m pip install -U pip wheel setuptools build twine -r requirements/test.in -c requirements/test.txt - name: Restore llhttp generated files uses: actions/download-artifact@v8 with: @@ -413,12 +401,9 @@ jobs: uses: actions/setup-python@v6 with: python-version: '3.12' - - name: Update pip, wheel, setuptools, build, twine - run: | - python -m pip install -U pip wheel setuptools build twine - name: Install dependencies run: | - python -Im pip install -r requirements/test.in -c requirements/test.txt + python -Im pip install -U pip wheel setuptools build twine -r requirements/test.in -c requirements/test.txt - name: Uninstall blocbuster run: python -m pip uninstall blockbuster -y - name: Restore llhttp generated files @@ -499,13 +484,10 @@ jobs: submodules: true - name: Setup Python uses: actions/setup-python@v6 - - name: Update pip, wheel, setuptools, build, twine - run: | - python -m pip install -U pip wheel setuptools build twine - - name: Install cython + - name: Install build tooling and cython run: >- python -m - pip install -r requirements/cython.in -c requirements/cython.txt + pip install -U pip wheel setuptools build twine -r requirements/cython.in -c requirements/cython.txt - name: Restore llhttp generated files uses: actions/download-artifact@v8 with: @@ -589,13 +571,10 @@ jobs: uses: actions/setup-python@v6 with: python-version: 3.x - - name: Update pip, wheel, setuptools, build, twine - run: | - python -m pip install -U pip wheel setuptools build twine - - name: Install cython + - name: Install build tooling and cython run: >- python -m - pip install -r requirements/cython.in -c requirements/cython.txt + pip install -U pip wheel setuptools build twine -r requirements/cython.in -c requirements/cython.txt - name: Restore llhttp generated files uses: actions/download-artifact@v8 with: diff --git a/CHANGES/12643.contrib.rst b/CHANGES/12643.contrib.rst new file mode 100644 index 00000000000..8477457ce16 --- /dev/null +++ b/CHANGES/12643.contrib.rst @@ -0,0 +1,5 @@ +Merged the "Update pip, wheel, setuptools, build, twine" step into +the following ``Install dependencies`` (or ``Install cython``) step +in every job in :file:`.github/workflows/ci-cd.yml`, so each job now +runs a single ``pip``/``uv pip install`` invocation instead of two +-- by :user:`bdraco`. From c291b26fdc59fcbb56edb07077d1b59fcdfa7fd1 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 17:34:54 +0000 Subject: [PATCH 131/191] [PR #12647/3c608498 backport][3.13] ci: fall back to plain build frontend for odd-arch wheels (#12648) Co-authored-by: J. Nick Koston --- .github/workflows/ci-cd.yml | 10 ++++++++-- CHANGES/12647.contrib.rst | 4 ++++ docs/spelling_wordlist.txt | 2 ++ 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 CHANGES/12647.contrib.rst diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index aec0cfac0f0..704cf815c0a 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -438,6 +438,9 @@ jobs: # Build emulated architectures only if QEMU is set, # use default "auto" otherwise echo "CIBW_ARCHS_LINUX=${{ matrix.qemu }}" >> $GITHUB_ENV + # Override pyproject.toml's `build[uv]`: the pypa odd-arch + # manylinux/musllinux containers do not ship `uv` preinstalled. + echo "CIBW_BUILD_FRONTEND=build" >> $GITHUB_ENV fi shell: bash - name: Setup Python @@ -464,8 +467,11 @@ jobs: with: # `build-frontend = "build[uv]"` (pyproject.toml) requires uv to be # available on the runner for Windows and macOS. Installing - # cibuildwheel with the `uv` extra bundles uv with it; Linux - # already has uv inside the manylinux/musllinux container. + # cibuildwheel with the `uv` extra bundles uv with it; the + # tested-arch manylinux/musllinux containers also ship uv + # preinstalled. The odd-arch containers do not, so the + # `Prepare emulation` step above sets `CIBW_BUILD_FRONTEND=build` + # for those QEMU matrix cells. extras: uv env: CIBW_SKIP: pp* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }} diff --git a/CHANGES/12647.contrib.rst b/CHANGES/12647.contrib.rst new file mode 100644 index 00000000000..5203ba0a395 --- /dev/null +++ b/CHANGES/12647.contrib.rst @@ -0,0 +1,4 @@ +Override ``CIBW_BUILD_FRONTEND=build`` on the QEMU-emulated odd-arch wheel +jobs so cibuildwheel falls back to plain pip, because the pypa +``manylinux``/``musllinux`` containers for those arches do not ship ``uv`` +preinstalled -- by :user:`bdraco`. diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 77c421e3e79..416e4f34615 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -67,6 +67,7 @@ charset charsetdetect chunked chunking +cibuildwheel CIMultiDict ClientSession cls @@ -255,6 +256,7 @@ py pydantic pyenv pyflakes +pypa pyright pytest Pytest From 730c54a0f71e8eb5c8678ea7b0b453c6733b138e Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 17:40:19 +0000 Subject: [PATCH 132/191] [PR #12647/3c608498 backport][3.14] ci: fall back to plain build frontend for odd-arch wheels (#12649) Co-authored-by: J. Nick Koston --- .github/workflows/ci-cd.yml | 10 ++++++++-- CHANGES/12647.contrib.rst | 4 ++++ docs/spelling_wordlist.txt | 2 ++ 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 CHANGES/12647.contrib.rst diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index c954482a635..44246de8533 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -565,6 +565,9 @@ jobs: # Build emulated architectures only if QEMU is set, # use default "auto" otherwise echo "CIBW_ARCHS_LINUX=${{ matrix.qemu }}" >> $GITHUB_ENV + # Override pyproject.toml's `build[uv]`: the pypa odd-arch + # manylinux/musllinux containers do not ship `uv` preinstalled. + echo "CIBW_BUILD_FRONTEND=build" >> $GITHUB_ENV fi shell: bash - name: Setup Python @@ -588,8 +591,11 @@ jobs: with: # `build-frontend = "build[uv]"` (pyproject.toml) requires uv to be # available on the runner for Windows and macOS. Installing - # cibuildwheel with the `uv` extra bundles uv with it; Linux - # already has uv inside the manylinux/musllinux container. + # cibuildwheel with the `uv` extra bundles uv with it; the + # tested-arch manylinux/musllinux containers also ship uv + # preinstalled. The odd-arch containers do not, so the + # `Prepare emulation` step above sets `CIBW_BUILD_FRONTEND=build` + # for those QEMU matrix cells. extras: uv env: CIBW_SKIP: pp* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }} diff --git a/CHANGES/12647.contrib.rst b/CHANGES/12647.contrib.rst new file mode 100644 index 00000000000..5203ba0a395 --- /dev/null +++ b/CHANGES/12647.contrib.rst @@ -0,0 +1,4 @@ +Override ``CIBW_BUILD_FRONTEND=build`` on the QEMU-emulated odd-arch wheel +jobs so cibuildwheel falls back to plain pip, because the pypa +``manylinux``/``musllinux`` containers for those arches do not ship ``uv`` +preinstalled -- by :user:`bdraco`. diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 16f7da5ebe4..b6323e39882 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -68,6 +68,7 @@ charset charsetdetect chunked chunking +cibuildwheel CIMultiDict ClientSession cls @@ -263,6 +264,7 @@ py pydantic pyenv pyflakes +pypa pyright pytest Pytest From 7baa5923253b866649c6e1a58ca554fbf3033b25 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 19 May 2026 20:02:44 +0100 Subject: [PATCH 133/191] Add output_size property (#12452) (#12650) (cherry picked from commit 37cd4a8edcbfe543067a96e36f4da3e9395328a3) --- CHANGES/12452.feature.rst | 2 + aiohttp/client_reqrep.py | 39 ++++++- docs/client_reference.rst | 24 ++++ tests/test_client_functional.py | 170 ++++++++++++++++++++++++++++ tests/test_client_proto.py | 10 ++ tests/test_client_request.py | 3 + tests/test_client_response.py | 190 ++++++++++++++++++++++++++++++++ tests/test_proxy.py | 31 ++++++ 8 files changed, 467 insertions(+), 2 deletions(-) create mode 100644 CHANGES/12452.feature.rst diff --git a/CHANGES/12452.feature.rst b/CHANGES/12452.feature.rst new file mode 100644 index 00000000000..ad4936323da --- /dev/null +++ b/CHANGES/12452.feature.rst @@ -0,0 +1,2 @@ +Added :attr:`~aiohttp.ClientResponse.output_size` and +:attr:`~aiohttp.ClientResponse.upload_complete` -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index 0353ec34f03..4aeae08ac19 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -304,6 +304,9 @@ class ClientResponse(HeadersMixin): _resolve_charset: Callable[["ClientResponse", bytes], str] = lambda *_: "utf-8" __writer: Optional["asyncio.Task[None]"] = None + _stream_writer: Optional[AbstractStreamWriter] = None + _output_size: int = 0 + _upload_complete: Optional[asyncio.Future[None]] = None def __init__( self, @@ -317,6 +320,7 @@ def __init__( traces: list["Trace"], loop: asyncio.AbstractEventLoop, session: "ClientSession", + stream_writer: AbstractStreamWriter, ) -> None: # URL forbids subclasses, so a simple type check is enough. assert type(url) is URL @@ -325,7 +329,10 @@ def __init__( self._real_url = url self._url = url.with_fragment(None) if url.raw_fragment else url - if writer is not None: + if writer is None: # Request already sent + self._output_size = stream_writer.output_size + else: + self._stream_writer = stream_writer self._writer = writer if continue100 is not None: self._continue = continue100 @@ -346,6 +353,11 @@ def __init__( def __reset_writer(self, _: object = None) -> None: self.__writer = None + if self._stream_writer is not None: + self._output_size = self._stream_writer.output_size + self._stream_writer = None + if self._upload_complete is not None and not self._upload_complete.done(): + self._upload_complete.set_result(None) @property def _writer(self) -> Optional["asyncio.Task[None]"]: @@ -366,10 +378,29 @@ def _writer(self, writer: Optional["asyncio.Task[None]"]) -> None: return if writer.done(): # The writer is already done, so we can clear it immediately. - self.__writer = None + self.__reset_writer() else: writer.add_done_callback(self.__reset_writer) + @property + def output_size(self) -> int: + """Number of bytes sent for this request.""" + if self._stream_writer is not None: + return self._stream_writer.output_size + return self._output_size + + @property + def upload_complete(self) -> "asyncio.Future[None]": + """Future set when the request body has been fully sent. + + Already done when the request had no body or was written eagerly. + """ + if self._upload_complete is None: + self._upload_complete = self._loop.create_future() + if self._stream_writer is None: # upload already finished + self._upload_complete.set_result(None) + return self._upload_complete + @property def cookies(self) -> SimpleCookie: if self._cookies is None: @@ -652,6 +683,9 @@ async def _wait_released(self) -> None: def _cleanup_writer(self) -> None: if self.__writer is not None: self.__writer.cancel() + if self._stream_writer is not None: + self._output_size = self._stream_writer.output_size + self._stream_writer = None self._session = None def _notify_content(self) -> None: @@ -1487,6 +1521,7 @@ async def send(self, conn: "Connection") -> "ClientResponse": traces=self._traces, loop=self.loop, session=self._session, + stream_writer=writer, ) return self.response diff --git a/docs/client_reference.rst b/docs/client_reference.rst index f9f3dfcfef8..1b1a29072c7 100644 --- a/docs/client_reference.rst +++ b/docs/client_reference.rst @@ -1579,6 +1579,30 @@ Response object .. versionadded:: 3.2 + .. attribute:: output_size + + Number of bytes sent for this request. + + Pair with :attr:`upload_complete` to display upload progress:: + + async with session.post(url, data=mpwriter) as resp: + while not resp.upload_complete.done(): + print(f"uploaded {resp.output_size} bytes") + await asyncio.sleep(0.5) + print(f"upload complete: {resp.output_size} bytes") + + .. versionadded:: 3.14 + + .. attribute:: upload_complete + + An :class:`asyncio.Future` set when the request body has been fully sent. + + Use ``await resp.upload_complete`` to block until the upload finishes, or + ``resp.upload_complete.done()`` to poll from a progress-sampling loop + (see :attr:`output_size`). + + .. versionadded:: 3.14 + .. attribute:: content_type Read-only property with *content* part of *Content-Type* header. diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 66b86eb64ac..972de1e0e1c 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -5876,3 +5876,173 @@ async def handler(request: web.Request) -> web.Response: data = await resp.content.read() assert resp.content.total_raw_bytes == len(data) assert resp.content.total_raw_bytes == int(resp.headers["Content-Length"]) + + +async def test_output_size_bytes(aiohttp_client: AiohttpClient) -> None: + async def handler(request: web.Request) -> web.Response: + await request.read() + return web.Response() + + app = web.Application() + app.router.add_post("/", handler) + client = await aiohttp_client(app) + + body = b"x" * 1024 + async with client.post("/", data=body) as resp: + assert resp.output_size >= len(body) + + +async def test_output_size_multipart(aiohttp_client: AiohttpClient) -> None: + async def handler(request: web.Request) -> web.Response: + await request.read() + return web.Response() + + app = web.Application() + app.router.add_post("/", handler) + client = await aiohttp_client(app) + + mpwriter = aiohttp.MultipartWriter("form-data") + mpwriter.append(b"x" * 4096) + mpwriter.append(b"y" * 2048) + expected_body_size = mpwriter.size + assert expected_body_size is not None + + async with client.post("/", data=mpwriter) as resp: + assert resp.output_size >= expected_body_size + + +async def test_output_size_keepalive_isolated( + aiohttp_client: AiohttpClient, +) -> None: + """Each request on a keep-alive connection has its own counter.""" + transports: set[object] = set() + + async def handler(request: web.Request) -> web.Response: + transports.add(request.transport) + await request.read() + return web.Response() + + app = web.Application() + app.router.add_post("/", handler) + connector = aiohttp.TCPConnector(limit=1, force_close=False) + client = await aiohttp_client(app, connector=connector) + body = b"x" * 65536 + + async with client.post("/", data=body) as resp1: + size1 = resp1.output_size + + async with client.post("/", data=body) as resp2: + size2 = resp2.output_size + + assert len(transports) == 1 # Check keep-alive worked. + assert size1 >= len(body) + assert size1 == size2 + + +async def test_output_size_progress(aiohttp_client: AiohttpClient) -> None: + """output_size advances by exactly one chunk per yield.""" + + async def handler(request: web.Request) -> web.StreamResponse: + response = web.StreamResponse() + await response.prepare(request) + # Flush headers + a chunk so resp.start() returns on the client + # side before we read the body. + await response.write(b"x") + await request.read() + return response + + app = web.Application() + app.router.add_post("/", handler) + client = await aiohttp_client(app) + + chunk_size = 4096 + chunk = b"z" * chunk_size + num_chunks = 8 + sample_taken = asyncio.Event() + next_chunk = asyncio.Event() + + async def gated_body() -> AsyncIterator[bytes]: + for _ in range(num_chunks): + yield chunk + sample_taken.clear() + next_chunk.set() + await sample_taken.wait() + + async with client.post("/", data=gated_body()) as resp: + samples: list[int] = [] + for _ in range(num_chunks): + await next_chunk.wait() + next_chunk.clear() + samples.append(resp.output_size) + assert not resp.upload_complete.done() + sample_taken.set() + await resp.upload_complete + assert resp.upload_complete.done() + await resp.read() + + # Each sample after the first reflects exactly one more chunk on the wire. + chunked_framing = len(f"{chunk_size:x}".encode()) + 4 + deltas = [samples[i] - samples[i - 1] for i in range(1, len(samples))] + assert deltas == [chunk_size + chunked_framing] * (num_chunks - 1) + + +async def test_output_size_get_request(aiohttp_client: AiohttpClient) -> None: + """GET request with no body still reports the request header byte count.""" + + async def handler(request: web.Request) -> web.Response: + return web.Response() + + app = web.Application() + app.router.add_get("/", handler) + client = await aiohttp_client(app) + + async with client.get("/") as resp: + assert resp.output_size >= 0 + + +async def test_output_size_writer_released(aiohttp_client: AiohttpClient) -> None: + """Writer is dropped once body upload completes; output_size survives.""" + + async def handler(request: web.Request) -> web.Response: + await request.read() + return web.Response() + + app = web.Application() + app.router.add_post("/", handler) + client = await aiohttp_client(app) + + body = b"x" * 1024 + async with client.post("/", data=body) as resp: + await resp.read() + assert resp._stream_writer is None + assert resp.output_size >= len(body) + + +async def test_upload_complete_no_body(aiohttp_client: AiohttpClient) -> None: + async def handler(request: web.Request) -> web.Response: + return web.Response() + + app = web.Application() + app.router.add_get("/", handler) + client = await aiohttp_client(app) + + async with client.get("/") as resp: + assert resp.upload_complete.done() + + +async def test_upload_complete_late_access(aiohttp_client: AiohttpClient) -> None: + """Accessing upload_complete after the upload finished returns a done future.""" + + async def handler(request: web.Request) -> web.Response: + await request.read() + return web.Response() + + app = web.Application() + app.router.add_post("/", handler) + client = await aiohttp_client(app) + + async with client.post("/", data=b"x" * 1024) as resp: + await resp.read() + # Writer task is done; future is created lazily on this first access. + assert resp._upload_complete is None + assert resp.upload_complete.done() diff --git a/tests/test_client_proto.py b/tests/test_client_proto.py index 2936aab021a..ed6e602b0d5 100644 --- a/tests/test_client_proto.py +++ b/tests/test_client_proto.py @@ -4,6 +4,7 @@ from yarl import URL from aiohttp import http +from aiohttp.abc import AbstractStreamWriter from aiohttp.client_exceptions import ClientOSError, ServerDisconnectedError from aiohttp.client_proto import ResponseHandler from aiohttp.client_reqrep import ClientResponse @@ -118,6 +119,9 @@ async def test_multiple_responses_one_byte_at_a_time( traces=[], loop=loop, session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) await response.start(conn) await response.read() == payload @@ -148,6 +152,9 @@ class PatchableHttpResponseParser(http.HttpResponseParser): traces=[], loop=loop, session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) await response.start(conn) await response.read() == b"ab" @@ -176,6 +183,9 @@ async def test_client_protocol_readuntil_eof(loop: asyncio.AbstractEventLoop) -> traces=[], loop=loop, session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) proto.set_response_params(read_until_eof=True) await response.start(conn) diff --git a/tests/test_client_request.py b/tests/test_client_request.py index 5f5cf4a849f..66240258620 100644 --- a/tests/test_client_request.py +++ b/tests/test_client_request.py @@ -1462,6 +1462,9 @@ async def send(self, conn): traces=self._traces, loop=self.loop, session=self._session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) self.response = resp nonlocal called diff --git a/tests/test_client_response.py b/tests/test_client_response.py index 4e7ae2fb1a6..7e683a8b9e8 100644 --- a/tests/test_client_response.py +++ b/tests/test_client_response.py @@ -13,6 +13,7 @@ import aiohttp from aiohttp import ClientSession, hdrs, http +from aiohttp.abc import AbstractStreamWriter from aiohttp.client_reqrep import ClientResponse, RequestInfo from aiohttp.helpers import TimerNoop from aiohttp.multipart import BadContentDispositionHeader @@ -47,6 +48,9 @@ async def test_http_processing_error(session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) loop.get_debug = mock.Mock() loop.get_debug.return_value = True @@ -75,6 +79,9 @@ def test_del(session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) loop.get_debug = mock.Mock() loop.get_debug.return_value = True @@ -102,6 +109,9 @@ def test_close(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._closed = False response._connection = mock.Mock() @@ -122,6 +132,9 @@ def test_wait_for_100_1(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) assert response._continue is not None response.close() @@ -138,6 +151,9 @@ def test_wait_for_100_2(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) assert response._continue is None response.close() @@ -154,6 +170,9 @@ def test_repr(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response.status = 200 response.reason = "Ok" @@ -171,6 +190,9 @@ def test_repr_non_ascii_url() -> None: traces=[], loop=mock.Mock(), session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) assert "" in repr(response) @@ -186,6 +208,9 @@ def test_repr_non_ascii_reason() -> None: traces=[], loop=mock.Mock(), session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response.reason = "\u03bb" assert "" in repr( @@ -204,6 +229,9 @@ def test_url_obj_deprecated() -> None: traces=[], loop=mock.Mock(), session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) with pytest.warns(DeprecationWarning): response.url_obj @@ -220,6 +248,9 @@ async def test_read_and_release_connection(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) def side_effect(*args, **kwargs): @@ -246,6 +277,9 @@ async def test_read_and_release_connection_with_error(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) content = response.content = mock.Mock() content.read.return_value = loop.create_future() @@ -267,6 +301,9 @@ async def test_release(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) fut = loop.create_future() fut.set_result(b"") @@ -296,6 +333,9 @@ def run(conn): traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._closed = False response._connection = conn @@ -316,6 +356,9 @@ async def test_response_eof(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._closed = False conn = response._connection = mock.Mock() @@ -337,6 +380,9 @@ async def test_response_eof_upgraded(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) conn = response._connection = mock.Mock() @@ -358,6 +404,9 @@ async def test_response_eof_after_connection_detach(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._closed = False conn = response._connection = mock.Mock() @@ -379,6 +428,9 @@ async def test_text(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) def side_effect(*args, **kwargs): @@ -406,6 +458,9 @@ async def test_text_bad_encoding(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) def side_effect(*args, **kwargs): @@ -437,6 +492,9 @@ async def test_text_badly_encoded_encoding_header(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) def side_effect(*args: object, **kwargs: object): @@ -466,6 +524,9 @@ async def test_text_custom_encoding(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) def side_effect(*args, **kwargs): @@ -497,6 +558,9 @@ async def test_text_charset_resolver(content_type: str, loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) def side_effect(*args, **kwargs): @@ -526,6 +590,9 @@ async def test_get_encoding_body_none(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) def side_effect(*args, **kwargs): @@ -556,6 +623,9 @@ async def test_text_after_read(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) def side_effect(*args, **kwargs): @@ -583,6 +653,9 @@ async def test_json(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) def side_effect(*args, **kwargs): @@ -610,6 +683,9 @@ async def test_json_extended_content_type(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) def side_effect(*args, **kwargs): @@ -639,6 +715,9 @@ async def test_json_custom_content_type(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) def side_effect(*args, **kwargs): @@ -666,6 +745,9 @@ async def test_json_custom_loader(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._headers = {"Content-Type": "application/json;charset=cp1251"} response._body = b"data" @@ -688,6 +770,9 @@ async def test_json_invalid_content_type(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._headers = {"Content-Type": "data/octet-stream"} response._body = b"" @@ -711,6 +796,9 @@ async def test_json_no_content(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._headers = {"Content-Type": "data/octet-stream"} response._body = b"" @@ -730,6 +818,9 @@ async def test_json_override_encoding(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) def side_effect(*args, **kwargs): @@ -759,6 +850,9 @@ def test_get_encoding_unknown(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._headers = {"Content-Type": "application/json"} @@ -776,6 +870,9 @@ def test_raise_for_status_2xx() -> None: traces=[], loop=mock.Mock(), session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response.status = 200 response.reason = "OK" @@ -793,6 +890,9 @@ def test_raise_for_status_4xx() -> None: traces=[], loop=mock.Mock(), session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response.status = 409 response.reason = "CONFLICT" @@ -814,6 +914,9 @@ def test_raise_for_status_4xx_without_reason() -> None: traces=[], loop=mock.Mock(), session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response.status = 404 response.reason = "" @@ -835,6 +938,9 @@ def test_resp_host() -> None: traces=[], loop=mock.Mock(), session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) assert "del-cl-resp.org" == response.host @@ -850,6 +956,9 @@ def test_content_type() -> None: traces=[], loop=mock.Mock(), session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._headers = {"Content-Type": "application/json;charset=cp1251"} @@ -867,6 +976,9 @@ def test_content_type_no_header() -> None: traces=[], loop=mock.Mock(), session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._headers = {} @@ -884,6 +996,9 @@ def test_charset() -> None: traces=[], loop=mock.Mock(), session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._headers = {"Content-Type": "application/json;charset=cp1251"} @@ -901,6 +1016,9 @@ def test_charset_no_header() -> None: traces=[], loop=mock.Mock(), session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._headers = {} @@ -918,6 +1036,9 @@ def test_charset_no_charset() -> None: traces=[], loop=mock.Mock(), session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._headers = {"Content-Type": "application/json"} @@ -935,6 +1056,9 @@ def test_content_disposition_full() -> None: traces=[], loop=mock.Mock(), session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._headers = { "Content-Disposition": 'attachment; filename="archive.tar.gz"; foo=bar' @@ -958,6 +1082,9 @@ def test_content_disposition_no_parameters() -> None: traces=[], loop=mock.Mock(), session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._headers = {"Content-Disposition": "attachment"} @@ -984,6 +1111,9 @@ def test_content_disposition_empty_parts(content_disposition: str) -> None: traces=[], loop=mock.Mock(), session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) h = {"Content-Disposition": content_disposition} response._headers = CIMultiDictProxy(CIMultiDict(h)) @@ -1005,6 +1135,9 @@ def test_content_disposition_no_header() -> None: traces=[], loop=mock.Mock(), session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._headers = {} @@ -1022,6 +1155,9 @@ def test_default_encoding_is_utf8() -> None: traces=[], loop=mock.Mock(), session=None, # type: ignore[arg-type] + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._headers = CIMultiDictProxy(CIMultiDict({})) response._body = b"" @@ -1042,6 +1178,9 @@ def test_response_request_info() -> None: traces=[], loop=mock.Mock(), session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) assert url == response.request_info.url assert "get" == response.request_info.method @@ -1061,6 +1200,9 @@ def test_request_info_in_exception() -> None: traces=[], loop=mock.Mock(), session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response.status = 409 response.reason = "CONFLICT" @@ -1082,6 +1224,9 @@ def test_no_redirect_history_in_exception() -> None: traces=[], loop=mock.Mock(), session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response.status = 409 response.reason = "CONFLICT" @@ -1105,6 +1250,9 @@ def test_redirect_history_in_exception() -> None: traces=[], loop=mock.Mock(), session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response.status = 409 response.reason = "CONFLICT" @@ -1119,6 +1267,9 @@ def test_redirect_history_in_exception() -> None: traces=[], loop=mock.Mock(), session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) hist_response._headers = hist_headers @@ -1148,6 +1299,9 @@ async def test_response_read_triggers_callback(loop, session) -> None: loop=loop, session=session, traces=[trace], + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) def side_effect(*args, **kwargs): @@ -1182,6 +1336,9 @@ def test_response_cookies( traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) cookies = response.cookies # Ensure the same cookies object is returned each time @@ -1202,6 +1359,9 @@ def test_response_real_url( traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) assert response.url == url.with_fragment(None) assert response.real_url == url @@ -1219,6 +1379,9 @@ def test_response_links_comma_separated(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._headers = CIMultiDict( [ @@ -1249,6 +1412,9 @@ def test_response_links_multiple_headers(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._headers = CIMultiDict( [ @@ -1274,6 +1440,9 @@ def test_response_links_no_rel(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._headers = CIMultiDict([("Link", "")]) assert response.links == { @@ -1293,6 +1462,9 @@ def test_response_links_quoted(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._headers = CIMultiDict( [ @@ -1316,6 +1488,9 @@ def test_response_links_relative(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._headers = CIMultiDict( [ @@ -1339,6 +1514,9 @@ def test_response_links_empty(loop, session) -> None: traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response._headers = CIMultiDict() assert response.links == {} @@ -1355,6 +1533,9 @@ def test_response_not_closed_after_get_ok(mocker) -> None: traces=[], loop=mock.Mock(), session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) response.status = 400 response.reason = "Bad Request" @@ -1389,6 +1570,9 @@ def test_response_duplicate_cookie_names( traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) # Set headers with duplicate cookie names but different domains @@ -1428,6 +1612,9 @@ def test_response_raw_cookie_headers_preserved( traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) # Set headers with multiple cookies @@ -1468,6 +1655,9 @@ def test_response_cookies_setter_updates_raw_headers( traces=[], loop=loop, session=session, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), ) # Create a SimpleCookie with some cookies diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 5599bf804f4..6f4b80c37b1 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -11,6 +11,7 @@ from yarl import URL import aiohttp +from aiohttp.abc import AbstractStreamWriter from aiohttp.client_reqrep import ClientRequest, ClientResponse, Fingerprint from aiohttp.connector import _SSL_CONTEXT_VERIFIED from aiohttp.helpers import TimerNoop @@ -269,6 +270,9 @@ def test_proxy_server_hostname_default( timer=TimerNoop(), traces=[], loop=self.loop, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), session=mock.Mock(), ) proxy_req.send = mock.AsyncMock(return_value=proxy_resp) @@ -338,6 +342,9 @@ def test_proxy_server_hostname_override( timer=TimerNoop(), traces=[], loop=self.loop, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), session=mock.Mock(), ) proxy_req.send = mock.AsyncMock(return_value=proxy_resp) @@ -417,6 +424,9 @@ def close(self) -> None: timer=TimerNoop(), traces=[], loop=self.loop, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), session=mock.Mock(), ) fingerprint_mock = mock.Mock(spec=Fingerprint, auto_spec=True) @@ -516,6 +526,9 @@ def test_https_connect( timer=TimerNoop(), traces=[], loop=self.loop, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), session=mock.Mock(), ) proxy_req.send = mock.AsyncMock(return_value=proxy_resp) @@ -583,6 +596,9 @@ def test_https_connect_certificate_error( timer=TimerNoop(), traces=[], loop=self.loop, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), session=mock.Mock(), ) proxy_req.send = mock.AsyncMock(return_value=proxy_resp) @@ -646,6 +662,9 @@ def test_https_connect_ssl_error( timer=TimerNoop(), traces=[], loop=self.loop, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), session=mock.Mock(), ) proxy_req.send = mock.AsyncMock(return_value=proxy_resp) @@ -709,6 +728,9 @@ def test_https_connect_http_proxy_error( timer=TimerNoop(), traces=[], loop=self.loop, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), session=mock.Mock(), ) proxy_req.send = mock.AsyncMock(return_value=proxy_resp) @@ -777,6 +799,9 @@ def test_https_connect_resp_start_error( timer=TimerNoop(), traces=[], loop=self.loop, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), session=mock.Mock(), ) proxy_req.send = mock.AsyncMock(return_value=proxy_resp) @@ -902,6 +927,9 @@ def test_https_connect_pass_ssl_context( timer=TimerNoop(), traces=[], loop=self.loop, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), session=mock.Mock(), ) proxy_req.send = mock.AsyncMock(return_value=proxy_resp) @@ -980,6 +1008,9 @@ def test_https_auth(self, start_connection: Any, ClientRequestMock: Any) -> None timer=TimerNoop(), traces=[], loop=self.loop, + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), session=mock.Mock(), ) proxy_req.send = mock.AsyncMock(return_value=proxy_resp) From 3c9ede21c1571366eec7fd85711beae538fe80cb Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 15:15:30 -0500 Subject: [PATCH 134/191] [PR #12651/f6912583 backport][3.13] ci: make the deploy job re-runnable after a partial failure (#12652) Co-authored-by: J. Nick Koston --- .github/workflows/ci-cd.yml | 26 ++++++++++++++++++++++++++ CHANGES/12651.contrib.rst | 5 +++++ docs/spelling_wordlist.txt | 2 ++ 3 files changed, 33 insertions(+) create mode 100644 CHANGES/12651.contrib.rst diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 704cf815c0a..058bd5234c7 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -522,7 +522,29 @@ jobs: - name: Collected dists run: | tree dist + - name: Check whether the GitHub Release already exists + # Allows re-running the deploy job after a partial failure (e.g. PyPI + # upload error) without the Make Release step failing with HTTP 422 + # because the tag/release was created on a prior attempt. Treat + # only the literal `release not found` reply as "does not exist"; + # other failures (auth, rate-limit, network) re-raise so the job + # fails loudly instead of falling through to Make Release. + id: gh-release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.ref_name }} + run: | + if gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" \ + >/dev/null 2>err; then + echo 'exists=true' >> "${GITHUB_OUTPUT}" + elif grep -qx 'release not found' err; then + echo 'exists=false' >> "${GITHUB_OUTPUT}" + else + cat err >&2 + exit 1 + fi - name: Make Release + if: steps.gh-release.outputs.exists != 'true' uses: aio-libs/create-release@v1.6.6 with: changes_file: CHANGES.rst @@ -538,6 +560,10 @@ jobs: - name: >- Publish 🐍📦 to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + with: + # Allow re-running the deploy job after a partial PyPI upload + # without failing on dists that were already published. + skip-existing: true - name: Sign the dists with Sigstore uses: sigstore/gh-action-sigstore-python@v3.0.1 diff --git a/CHANGES/12651.contrib.rst b/CHANGES/12651.contrib.rst new file mode 100644 index 00000000000..0318ed0d084 --- /dev/null +++ b/CHANGES/12651.contrib.rst @@ -0,0 +1,5 @@ +Allowed re-running the ``deploy`` job in ``.github/workflows/ci-cd.yml`` +after a partial release failure: the ``Make Release`` step now skips +when the GitHub Release already exists, and the PyPI publish step uses +``skip-existing`` so dists that were already uploaded on a prior +attempt do not break the retry -- by :user:`bdraco`. diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 416e4f34615..8fab90d3f7b 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -106,6 +106,7 @@ Dev dict Dict Discord +dists django Django dns @@ -257,6 +258,7 @@ pydantic pyenv pyflakes pypa +PyPI pyright pytest Pytest From 33c4789b7c08a6d121091615a9cda738b328b69a Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 15:18:51 -0500 Subject: [PATCH 135/191] [PR #12651/f6912583 backport][3.14] ci: make the deploy job re-runnable after a partial failure (#12653) Co-authored-by: J. Nick Koston --- .github/workflows/ci-cd.yml | 26 ++++++++++++++++++++++++++ CHANGES/12651.contrib.rst | 5 +++++ docs/spelling_wordlist.txt | 2 ++ 3 files changed, 33 insertions(+) create mode 100644 CHANGES/12651.contrib.rst diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 44246de8533..626731fcf08 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -646,7 +646,29 @@ jobs: - name: Collected dists run: | tree dist + - name: Check whether the GitHub Release already exists + # Allows re-running the deploy job after a partial failure (e.g. PyPI + # upload error) without the Make Release step failing with HTTP 422 + # because the tag/release was created on a prior attempt. Treat + # only the literal `release not found` reply as "does not exist"; + # other failures (auth, rate-limit, network) re-raise so the job + # fails loudly instead of falling through to Make Release. + id: gh-release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.ref_name }} + run: | + if gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" \ + >/dev/null 2>err; then + echo 'exists=true' >> "${GITHUB_OUTPUT}" + elif grep -qx 'release not found' err; then + echo 'exists=false' >> "${GITHUB_OUTPUT}" + else + cat err >&2 + exit 1 + fi - name: Make Release + if: steps.gh-release.outputs.exists != 'true' uses: aio-libs/create-release@v1.6.6 with: changes_file: CHANGES.rst @@ -662,6 +684,10 @@ jobs: - name: >- Publish 🐍📦 to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + with: + # Allow re-running the deploy job after a partial PyPI upload + # without failing on dists that were already published. + skip-existing: true - name: Sign the dists with Sigstore uses: sigstore/gh-action-sigstore-python@v3.3.0 diff --git a/CHANGES/12651.contrib.rst b/CHANGES/12651.contrib.rst new file mode 100644 index 00000000000..0318ed0d084 --- /dev/null +++ b/CHANGES/12651.contrib.rst @@ -0,0 +1,5 @@ +Allowed re-running the ``deploy`` job in ``.github/workflows/ci-cd.yml`` +after a partial release failure: the ``Make Release`` step now skips +when the GitHub Release already exists, and the PyPI publish step uses +``skip-existing`` so dists that were already uploaded on a prior +attempt do not break the retry -- by :user:`bdraco`. diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index b6323e39882..75719ce0318 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -108,6 +108,7 @@ Dev dict Dict Discord +dists django Django dns @@ -265,6 +266,7 @@ pydantic pyenv pyflakes pypa +PyPI pyright pytest Pytest From 2ab51fa50fe407fc297ae00fe5f373f7d571d531 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 21:57:48 +0000 Subject: [PATCH 136/191] [PR #12655/35a310d5 backport][3.13] ci: build armv7l wheels on native ARM runners (#12656) Co-authored-by: J. Nick Koston --- .github/workflows/ci-cd.yml | 9 +++++++-- CHANGES/12655.contrib.rst | 4 ++++ docs/spelling_wordlist.txt | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 CHANGES/12655.contrib.rst diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 058bd5234c7..aea2635dcc6 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -407,10 +407,15 @@ jobs: - os: ubuntu-latest qemu: s390x musl: musllinux - - os: ubuntu-latest + # armv7l builds on aarch64 hosts. We still register QEMU so + # binfmt picks up the 32-bit ARM userspace handler regardless of + # whether the host kernel has CONFIG_COMPAT enabled. Even with + # emulation, aarch64-on-aarch64 hosting beats x86_64 by a wide + # margin. + - os: ubuntu-24.04-arm qemu: armv7l musl: "" - - os: ubuntu-latest + - os: ubuntu-24.04-arm qemu: armv7l musl: musllinux - os: ubuntu-latest diff --git a/CHANGES/12655.contrib.rst b/CHANGES/12655.contrib.rst new file mode 100644 index 00000000000..f5c76cd4105 --- /dev/null +++ b/CHANGES/12655.contrib.rst @@ -0,0 +1,4 @@ +Switched the armv7l wheel builds onto GitHub's hosted ARM runners. The +32-bit ARM build still runs under QEMU, but the host is now aarch64 +rather than x86_64, so the emulation overhead drops sharply +-- by :user:`bdraco`. diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 8fab90d3f7b..9773d61ab3e 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -1,3 +1,4 @@ +aarch abc ABI addons From ec5c3fc2054041f9c30d7a89682c0f4c7dcb854f Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 22:09:39 +0000 Subject: [PATCH 137/191] [PR #12655/35a310d5 backport][3.14] ci: build armv7l wheels on native ARM runners (#12657) Co-authored-by: J. Nick Koston --- .github/workflows/ci-cd.yml | 9 +++++++-- CHANGES/12655.contrib.rst | 4 ++++ docs/spelling_wordlist.txt | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 CHANGES/12655.contrib.rst diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 626731fcf08..67923da42c0 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -534,10 +534,15 @@ jobs: - os: ubuntu-latest qemu: s390x musl: musllinux - - os: ubuntu-latest + # armv7l builds on aarch64 hosts. We still register QEMU so + # binfmt picks up the 32-bit ARM userspace handler regardless of + # whether the host kernel has CONFIG_COMPAT enabled. Even with + # emulation, aarch64-on-aarch64 hosting beats x86_64 by a wide + # margin. + - os: ubuntu-24.04-arm qemu: armv7l musl: "" - - os: ubuntu-latest + - os: ubuntu-24.04-arm qemu: armv7l musl: musllinux - os: ubuntu-latest diff --git a/CHANGES/12655.contrib.rst b/CHANGES/12655.contrib.rst new file mode 100644 index 00000000000..f5c76cd4105 --- /dev/null +++ b/CHANGES/12655.contrib.rst @@ -0,0 +1,4 @@ +Switched the armv7l wheel builds onto GitHub's hosted ARM runners. The +32-bit ARM build still runs under QEMU, but the host is now aarch64 +rather than x86_64, so the emulation overhead drops sharply +-- by :user:`bdraco`. diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 75719ce0318..c10f10fdce4 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -1,3 +1,4 @@ +aarch abc ABI addons From 79d80b58a9ce03e35527a6031e41838a3f24a53d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 22:12:18 +0000 Subject: [PATCH 138/191] Bump yarl from 1.22.0 to 1.24.2 (#12659) [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=yarl&package-manager=pip&previous-version=1.22.0&new-version=1.24.2)](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/base-ft.txt | 2 +- requirements/base.txt | 2 +- requirements/constraints.txt | 2 +- requirements/dev.txt | 2 +- requirements/runtime-deps.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt index 972f93541f8..d15daa320f9 100644 --- a/requirements/base-ft.txt +++ b/requirements/base-ft.txt @@ -47,5 +47,5 @@ typing-extensions==4.15.0 ; python_version < "3.13" # -r requirements/runtime-deps.in # aiosignal # multidict -yarl==1.22.0 +yarl==1.24.2 # via -r requirements/runtime-deps.in diff --git a/requirements/base.txt b/requirements/base.txt index e0edceac79c..eaa71549020 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -49,5 +49,5 @@ typing-extensions==4.15.0 ; python_version < "3.13" # multidict uvloop==0.22.1 ; platform_system != "Windows" and implementation_name == "cpython" # via -r requirements/base.in -yarl==1.22.0 +yarl==1.24.2 # via -r requirements/runtime-deps.in diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 184499ce80e..676f24d00be 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -295,7 +295,7 @@ wait-for-it==2.3.0 # via -r requirements/test-common.in wheel==0.47.0 # via pip-tools -yarl==1.22.0 +yarl==1.24.2 # via -r requirements/runtime-deps.in zlib-ng==1.0.0 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index 0ecd8ce83ae..69f0bc8d4e5 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -285,7 +285,7 @@ wait-for-it==2.3.0 # via -r requirements/test-common.in wheel==0.47.0 # via pip-tools -yarl==1.22.0 +yarl==1.24.2 # via -r requirements/runtime-deps.in zlib-ng==1.0.0 # via diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index 4bba60d46ed..549c5576f4d 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -43,5 +43,5 @@ typing-extensions==4.15.0 ; python_version < "3.13" # -r requirements/runtime-deps.in # aiosignal # multidict -yarl==1.22.0 +yarl==1.24.2 # via -r requirements/runtime-deps.in diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index abbf0178d64..a103d39b26b 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -157,7 +157,7 @@ typing-inspection==0.4.2 # via pydantic wait-for-it==2.3.0 # via -r requirements/test-common.in -yarl==1.22.0 +yarl==1.24.2 # via -r requirements/runtime-deps.in zlib-ng==1.0.0 # via -r requirements/test-common.in diff --git a/requirements/test.txt b/requirements/test.txt index 396d0469999..6449ddcfb14 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -159,7 +159,7 @@ uvloop==0.22.1 ; platform_system != "Windows" and implementation_name == "cpytho # via -r requirements/base.in wait-for-it==2.3.0 # via -r requirements/test-common.in -yarl==1.22.0 +yarl==1.24.2 # via -r requirements/runtime-deps.in zlib-ng==1.0.0 # via -r requirements/test-common.in From 491a818bc42a73862fc2e6d019bb295cf52a343b Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Wed, 20 May 2026 01:52:06 +0100 Subject: [PATCH 139/191] Fix C parser not rejecting response body when none expected (#10587) (#12660) (cherry picked from commit f69522cbfe6191bb60a926b5c313590f00349e9a) --- CHANGES/10587.bugfix.rst | 1 + aiohttp/_http_parser.pyx | 27 ++++++++++++++------------- tests/test_http_parser.py | 16 +++++++++++++++- 3 files changed, 30 insertions(+), 14 deletions(-) create mode 100644 CHANGES/10587.bugfix.rst diff --git a/CHANGES/10587.bugfix.rst b/CHANGES/10587.bugfix.rst new file mode 100644 index 00000000000..f60e1d17c5f --- /dev/null +++ b/CHANGES/10587.bugfix.rst @@ -0,0 +1 @@ +Fixed the C parser failing to reject a response with a body when none was expected -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index 146822b4887..344be3123b0 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -502,11 +502,14 @@ cdef class HttpParser: upgrade, chunked) if ( - ULLONG_MAX > self._cparser.content_length > 0 or chunked or - self._cparser.method == cparser.HTTP_CONNECT or - (self._cparser.status_code >= 199 and - self._cparser.content_length == 0 and - self._read_until_eof) + self._response_with_body + and ( + ULLONG_MAX > self._cparser.content_length > 0 or chunked or + self._cparser.method == cparser.HTTP_CONNECT or + (self._cparser.status_code >= 199 and + self._cparser.content_length == 0 and + self._read_until_eof) + ) ): payload = StreamReader( self.protocol, timer=self._timer, loop=self._loop, @@ -518,9 +521,6 @@ cdef class HttpParser: if encoding is not None and self._auto_decompress: self._payload = DeflateBuffer(payload, encoding, max_decompress_size=self._limit) - if not self._response_with_body: - payload = EMPTY_PAYLOAD - self._messages.append((msg, payload)) cdef _on_message_complete(self): @@ -847,8 +847,9 @@ cdef int cb_on_headers_complete(cparser.llhttp_t* parser) except -1: else: if pyparser._upgraded or pyparser._cparser.method == cparser.HTTP_CONNECT: return 2 - else: - return 0 + if not pyparser._response_with_body: + return 1 + return 0 cdef int cb_on_body(cparser.llhttp_t* parser, @@ -923,7 +924,6 @@ cdef parser_error_from_errno(cparser.llhttp_t* parser, data, pointer): cparser.HPE_CB_MESSAGE_COMPLETE, cparser.HPE_CB_CHUNK_HEADER, cparser.HPE_CB_CHUNK_COMPLETE, - cparser.HPE_INVALID_CONSTANT, cparser.HPE_INVALID_HEADER_TOKEN, cparser.HPE_INVALID_CONTENT_LENGTH, cparser.HPE_INVALID_CHUNK_SIZE, @@ -933,8 +933,9 @@ cdef parser_error_from_errno(cparser.llhttp_t* parser, data, pointer): elif errno == cparser.HPE_INVALID_METHOD: return BadHttpMethod(error=err_msg) elif errno in {cparser.HPE_INVALID_STATUS, - cparser.HPE_INVALID_VERSION}: - return BadStatusLine(error=err_msg) + cparser.HPE_INVALID_VERSION, + cparser.HPE_INVALID_CONSTANT}: + return BadStatusLine(error=f"Bad status line:\n {err_msg}") elif errno == cparser.HPE_INVALID_URL: return InvalidURLError(err_msg) diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index 99e952ac882..625c34533bc 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -1910,7 +1910,21 @@ def test_parse_payload_response_without_body( assert payload.is_eof() -def test_parse_length_payload(response) -> None: +async def test_parse_payload_response_with_invalid_body( + protocol: BaseProtocol, + response_cls: type[HttpResponseParser], +) -> None: + loop = asyncio.get_running_loop() + parser = response_cls(protocol, loop, 2**16, response_with_body=False) + text = ( + b"HTTP/1.1 200 Ok\r\nTransfer-Encoding: chunked\r\n\r\n" + b"7\r\nchunked\r\n0\r\n\r\n" + ) + with pytest.raises(http_exceptions.BadHttpMessage, match="status line"): + parser.feed_data(text)[0][0] + + +def test_parse_length_payload(response: HttpResponseParser) -> None: text = b"HTTP/1.1 200 Ok\r\ncontent-length: 4\r\n\r\n" msg, payload = response.feed_data(text)[0][0] assert not payload.is_eof() From 7dde3a295ed4b4a22cb52cc2580bd3ac91fc1ae7 Mon Sep 17 00:00:00 2001 From: Omkar Kabde Date: Wed, 20 May 2026 18:24:42 +0530 Subject: [PATCH 140/191] [PR #12615/421eb60cc backport][3.14] Render the threat model in the docs (#12661) Co-authored-by: Sam Bull --- CHANGES/12549.doc.rst | 1 + CONTRIBUTORS.txt | 1 + THREAT_MODEL.md | 4 +--- docs/conf.py | 19 +++++++++++++++++++ docs/index.rst | 1 + docs/threat_model.md | 2 ++ requirements/constraints.txt | 30 +++++++++++++++++++++++++----- requirements/dev.txt | 30 +++++++++++++++++++++++++----- requirements/doc-spelling.txt | 24 +++++++++++++++++++++++- requirements/doc.in | 2 ++ requirements/doc.txt | 24 +++++++++++++++++++++++- 11 files changed, 123 insertions(+), 15 deletions(-) create mode 100644 CHANGES/12549.doc.rst create mode 100644 docs/threat_model.md diff --git a/CHANGES/12549.doc.rst b/CHANGES/12549.doc.rst new file mode 100644 index 00000000000..0288c33cba5 --- /dev/null +++ b/CHANGES/12549.doc.rst @@ -0,0 +1 @@ +Added the :doc:`threat_model` to the Sphinx documentation -- by :user:`omkar-334`. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 530b6683d54..4180c590f44 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -283,6 +283,7 @@ Nikolay Tiunov Nándor Mátravölgyi Oisin Aylward Olaf Conradi +Omkar Kabde Pahaz Blinov Panagiotis Kolokotronis Pankaj Pandey diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index 1c10ce1e219..3f48765e249 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -1,4 +1,4 @@ -# aiohttp Threat Model +# Threat Model This document is a STRIDE-based threat model for the [aiohttp](https://github.com/aio-libs/aiohttp) library. It is a living document @@ -310,5 +310,3 @@ into `StreamReader`) is then handed to `web_protocol.RequestHandler` and request parser (strict). These are all currently in place; this section assumes no regression. - ---- diff --git a/docs/conf.py b/docs/conf.py index 184ad7816cf..c5a4b921a40 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -59,6 +59,8 @@ "sphinx.ext.intersphinx", "sphinx.ext.viewcode", # Third-party extensions: + "myst_parser", # renders Markdown sources (e.g. ``THREAT_MODEL.md``) + "sphinxcontrib.mermaid", # renders the Mermaid flowcharts in ``THREAT_MODEL.md`` "sphinxcontrib.towncrier.ext", # provides `towncrier-draft-entries` directive ] @@ -70,6 +72,23 @@ except ImportError: pass +spelling_exclude_patterns = [ + # THREAT_MODEL.md is already spell-checked by the codespell pre-commit hook. + # The spelling builder additionally mis-tokenises its ``**S**poofing`` STRIDE + # list into non-words, so skip the threat model here to avoid double coverage. + "threat_model.md", +] + + +# -- MyST (Markdown) configuration ---------------------------------------- + +# ``THREAT_MODEL.md`` lives at the repo root and is surfaced through +# ``docs/threat_model.md``. +myst_heading_anchors = 3 # anchors for h1-h3 so the in-page section links work +myst_fence_as_directive = ["mermaid"] # render ```mermaid fences as diagrams + +# TODO: Remove this option once THREAT_MODEL.md is complete. +suppress_warnings = ["myst.xref_missing"] intersphinx_mapping = { "pytest": ("http://docs.pytest.org/en/latest/", None), diff --git a/docs/index.rst b/docs/index.rst index f9c4a4b2c54..8387ae186b5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -233,3 +233,4 @@ Table Of Contents misc external contributing + threat_model diff --git a/docs/threat_model.md b/docs/threat_model.md new file mode 100644 index 00000000000..f585a70ff9e --- /dev/null +++ b/docs/threat_model.md @@ -0,0 +1,2 @@ +```{include} ../THREAT_MODEL.md +``` diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 676f24d00be..7187b799f9f 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -25,7 +25,9 @@ async-timeout==5.0.1 ; python_version < "3.11" # -r requirements/runtime-deps.in # valkey attrs==26.1.0 - # via -r requirements/runtime-deps.in + # via + # -r requirements/runtime-deps.in + # myst-parser babel==2.18.0 # via sphinx backports-zstd==1.3.0 ; implementation_name == "cpython" and python_version < "3.14" @@ -67,7 +69,9 @@ cython==3.2.4 distlib==0.4.0 # via virtualenv docutils==0.21.2 - # via sphinx + # via + # myst-parser + # sphinx exceptiongroup==1.3.1 # via pytest execnet==2.1.2 @@ -105,14 +109,21 @@ isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython" # -r requirements/test-common.in jinja2==3.1.6 # via + # myst-parser # sphinx + # sphinxcontrib-mermaid # towncrier librt==0.11.0 # via mypy -markdown-it-py==4.2.0 - # via rich +markdown-it-py==3.0.0 + # via + # mdit-py-plugins + # myst-parser + # rich markupsafe==3.0.3 # via jinja2 +mdit-py-plugins==0.6.1 + # via myst-parser mdurl==0.1.2 # via markdown-it-py multidict==6.7.1 @@ -126,6 +137,8 @@ mypy==2.1.0 ; implementation_name == "cpython" # -r requirements/test-common.in mypy-extensions==1.1.0 # via mypy +myst-parser==4.0.1 + # via -r requirements/doc.in nodeenv==1.10.0 # via pre-commit packaging==26.2 @@ -208,7 +221,10 @@ python-on-whales==0.81.0 # -r requirements/lint.in # -r requirements/test-common.in pyyaml==6.0.3 - # via pre-commit + # via + # myst-parser + # pre-commit + # sphinxcontrib-mermaid re-assert==1.1.0 # via -r requirements/test-common.in regex==2026.5.9 @@ -230,6 +246,8 @@ snowballstemmer==3.0.1 sphinx==8.1.3 # via # -r requirements/doc.in + # myst-parser + # sphinxcontrib-mermaid # sphinxcontrib-spelling # sphinxcontrib-towncrier sphinxcontrib-applehelp==2.0.0 @@ -240,6 +258,8 @@ sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx +sphinxcontrib-mermaid==2.0.2 + # via -r requirements/doc.in sphinxcontrib-qthelp==2.0.0 # via sphinx sphinxcontrib-serializinghtml==2.0.0 diff --git a/requirements/dev.txt b/requirements/dev.txt index 69f0bc8d4e5..fbc7bcf0649 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -25,7 +25,9 @@ async-timeout==5.0.1 ; python_version < "3.11" # -r requirements/runtime-deps.in # valkey attrs==26.1.0 - # via -r requirements/runtime-deps.in + # via + # -r requirements/runtime-deps.in + # myst-parser babel==2.18.0 # via sphinx backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" @@ -65,7 +67,9 @@ cryptography==48.0.0 distlib==0.4.0 # via virtualenv docutils==0.21.2 - # via sphinx + # via + # myst-parser + # sphinx exceptiongroup==1.3.1 # via pytest execnet==2.1.2 @@ -103,14 +107,21 @@ isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython" # -r requirements/test-common.in jinja2==3.1.6 # via + # myst-parser # sphinx + # sphinxcontrib-mermaid # towncrier librt==0.11.0 # via mypy -markdown-it-py==4.2.0 - # via rich +markdown-it-py==3.0.0 + # via + # mdit-py-plugins + # myst-parser + # rich markupsafe==3.0.3 # via jinja2 +mdit-py-plugins==0.6.1 + # via myst-parser mdurl==0.1.2 # via markdown-it-py multidict==6.7.1 @@ -123,6 +134,8 @@ mypy==2.1.0 ; implementation_name == "cpython" # -r requirements/test-common.in mypy-extensions==1.1.0 # via mypy +myst-parser==4.0.1 + # via -r requirements/doc.in nodeenv==1.10.0 # via pre-commit packaging==26.2 @@ -203,7 +216,10 @@ python-on-whales==0.81.0 # -r requirements/lint.in # -r requirements/test-common.in pyyaml==6.0.3 - # via pre-commit + # via + # myst-parser + # pre-commit + # sphinxcontrib-mermaid re-assert==1.1.0 # via -r requirements/test-common.in regex==2026.5.9 @@ -223,6 +239,8 @@ snowballstemmer==3.0.1 sphinx==8.1.3 # via # -r requirements/doc.in + # myst-parser + # sphinxcontrib-mermaid # sphinxcontrib-towncrier sphinxcontrib-applehelp==2.0.0 # via sphinx @@ -232,6 +250,8 @@ sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx +sphinxcontrib-mermaid==2.0.2 + # via -r requirements/doc.in sphinxcontrib-qthelp==2.0.0 # via sphinx sphinxcontrib-serializinghtml==2.0.0 diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index eb528786532..e653ba3adea 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -17,23 +17,41 @@ charset-normalizer==3.4.7 click==8.4.0 # via towncrier docutils==0.21.2 - # via sphinx + # via + # myst-parser + # sphinx idna==3.15 # via requests imagesize==2.0.0 # via sphinx jinja2==3.1.6 # via + # myst-parser # sphinx + # sphinxcontrib-mermaid # towncrier +markdown-it-py==3.0.0 + # via + # mdit-py-plugins + # myst-parser markupsafe==3.0.3 # via jinja2 +mdit-py-plugins==0.6.1 + # via myst-parser +mdurl==0.1.2 + # via markdown-it-py +myst-parser==4.0.1 + # via -r requirements/doc.in packaging==26.2 # via sphinx pyenchant==3.3.0 # via sphinxcontrib-spelling pygments==2.20.0 # via sphinx +pyyaml==6.0.3 + # via + # myst-parser + # sphinxcontrib-mermaid requests==2.34.2 # via # sphinx @@ -43,6 +61,8 @@ snowballstemmer==3.0.1 sphinx==8.1.3 # via # -r requirements/doc.in + # myst-parser + # sphinxcontrib-mermaid # sphinxcontrib-spelling # sphinxcontrib-towncrier sphinxcontrib-applehelp==2.0.0 @@ -53,6 +73,8 @@ sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx +sphinxcontrib-mermaid==2.0.2 + # via -r requirements/doc.in sphinxcontrib-qthelp==2.0.0 # via sphinx sphinxcontrib-serializinghtml==2.0.0 diff --git a/requirements/doc.in b/requirements/doc.in index 15017b083d3..7106c3808d9 100644 --- a/requirements/doc.in +++ b/requirements/doc.in @@ -1,4 +1,6 @@ aiohttp-theme +myst-parser sphinx +sphinxcontrib-mermaid sphinxcontrib-towncrier towncrier diff --git a/requirements/doc.txt b/requirements/doc.txt index 64ba54abc64..8466dd55845 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -17,21 +17,39 @@ charset-normalizer==3.4.7 click==8.4.0 # via towncrier docutils==0.21.2 - # via sphinx + # via + # myst-parser + # sphinx idna==3.15 # via requests imagesize==2.0.0 # via sphinx jinja2==3.1.6 # via + # myst-parser # sphinx + # sphinxcontrib-mermaid # towncrier +markdown-it-py==3.0.0 + # via + # mdit-py-plugins + # myst-parser markupsafe==3.0.3 # via jinja2 +mdit-py-plugins==0.6.1 + # via myst-parser +mdurl==0.1.2 + # via markdown-it-py +myst-parser==4.0.1 + # via -r requirements/doc.in packaging==26.2 # via sphinx pygments==2.20.0 # via sphinx +pyyaml==6.0.3 + # via + # myst-parser + # sphinxcontrib-mermaid requests==2.34.2 # via sphinx snowballstemmer==3.0.1 @@ -39,6 +57,8 @@ snowballstemmer==3.0.1 sphinx==8.1.3 # via # -r requirements/doc.in + # myst-parser + # sphinxcontrib-mermaid # sphinxcontrib-towncrier sphinxcontrib-applehelp==2.0.0 # via sphinx @@ -48,6 +68,8 @@ sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx +sphinxcontrib-mermaid==2.0.2 + # via -r requirements/doc.in sphinxcontrib-qthelp==2.0.0 # via sphinx sphinxcontrib-serializinghtml==2.0.0 From 79bdecdbe330b6a7925fc81d12d1cdb11a0af02f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 20 May 2026 11:56:28 -0500 Subject: [PATCH 141/191] [PR #12174/ca9d73dd backport][3.14] feat: add cookies and host_only_cookies properties to CookieJar (#12662) Co-authored-by: Br1an <932039080@qq.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Fixes #3951 --- CHANGES/3951.feature.rst | 1 + CONTRIBUTORS.txt | 1 + aiohttp/abc.py | 13 +++++++++- aiohttp/cookiejar.py | 21 ++++++++++++++++ docs/client_reference.rst | 15 +++++++++++ tests/test_client_session.py | 11 ++++++++ tests/test_cookiejar.py | 49 ++++++++++++++++++++++++++++++++++++ 7 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 CHANGES/3951.feature.rst diff --git a/CHANGES/3951.feature.rst b/CHANGES/3951.feature.rst new file mode 100644 index 00000000000..ba127c20a83 --- /dev/null +++ b/CHANGES/3951.feature.rst @@ -0,0 +1 @@ +Added :py:attr:`~aiohttp.CookieJar.cookies` and :py:attr:`~aiohttp.CookieJar.host_only_cookies` read-only properties to :py:class:`~aiohttp.CookieJar` exposing the stored cookies with their full attributes -- by :user:`Br1an67`. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 4180c590f44..ba7ce91cec1 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -65,6 +65,7 @@ Benedikt Reinartz Bob Haddleton Boris Feld Boyi Chen +Br1an67 Brett Cannon Brett Higgins Brian Bouterse diff --git a/aiohttp/abc.py b/aiohttp/abc.py index d5dcdc00233..b67287318a9 100644 --- a/aiohttp/abc.py +++ b/aiohttp/abc.py @@ -3,7 +3,8 @@ import socket from abc import ABC, abstractmethod from collections.abc import Awaitable, Callable, Generator, Iterable, Sequence, Sized -from http.cookies import BaseCookie, Morsel +from http.cookies import BaseCookie, Morsel, SimpleCookie +from types import MappingProxyType from typing import TYPE_CHECKING, Any, TypedDict from multidict import CIMultiDict @@ -173,6 +174,16 @@ def unsafe(self) -> bool: def quote_cookie(self) -> bool: """Return True if cookies should be quoted.""" + @property + @abstractmethod + def cookies(self) -> MappingProxyType[tuple[str, str], SimpleCookie]: + """Return the cookies stored in this jar.""" + + @property + @abstractmethod + def host_only_cookies(self) -> frozenset[tuple[str, str]]: + """Return the host-only cookies stored in this jar.""" + @abstractmethod def clear(self, predicate: ClearCookiePredicate | None = None) -> None: """Clear all cookies if no predicate is passed.""" diff --git a/aiohttp/cookiejar.py b/aiohttp/cookiejar.py index cd19c9e79cc..e1579c0ed4c 100644 --- a/aiohttp/cookiejar.py +++ b/aiohttp/cookiejar.py @@ -14,6 +14,7 @@ from collections import defaultdict from collections.abc import Iterable, Iterator, Mapping from http.cookies import BaseCookie, Morsel, SimpleCookie +from types import MappingProxyType from typing import Union from yarl import URL @@ -156,6 +157,16 @@ def unsafe(self) -> bool: def quote_cookie(self) -> bool: return self._quote_cookie + @property + def cookies(self) -> MappingProxyType[tuple[str, str], SimpleCookie]: + """Return the cookies stored in this jar.""" + return MappingProxyType(self._cookies) + + @property + def host_only_cookies(self) -> frozenset[tuple[str, str]]: + """Return the host-only cookies stored in this jar.""" + return frozenset(self._host_only_cookies) + def save(self, file_path: PathLike) -> None: """Save cookies to a file using JSON format. @@ -626,6 +637,16 @@ def unsafe(self) -> bool: def quote_cookie(self) -> bool: return True + @property + def cookies(self) -> MappingProxyType[tuple[str, str], SimpleCookie]: + """Return an empty mapping.""" + return MappingProxyType({}) + + @property + def host_only_cookies(self) -> frozenset[tuple[str, str]]: + """Return an empty frozenset.""" + return frozenset() + def clear(self, predicate: ClearCookiePredicate | None = None) -> None: pass diff --git a/docs/client_reference.rst b/docs/client_reference.rst index 1b1a29072c7..860e6fba119 100644 --- a/docs/client_reference.rst +++ b/docs/client_reference.rst @@ -2586,6 +2586,21 @@ Utilities .. versionadded:: 4.0 + .. attribute:: cookies + + A read-only view of the jar's cookies as a + :class:`~types.MappingProxyType` mapping ``(domain, path)`` tuples + to :class:`~http.cookies.SimpleCookie` instances. + + .. versionadded:: 3.14 + + .. attribute:: host_only_cookies + + A :class:`frozenset` of ``(domain, name)`` tuples indicating which + cookies are host-only (not sent to subdomains). + + .. versionadded:: 3.14 + .. class:: DummyCookieJar(*, loop=None) diff --git a/tests/test_client_session.py b/tests/test_client_session.py index 1060d1f2516..be9e77dfcc0 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -8,6 +8,7 @@ from collections import deque from collections.abc import Awaitable, Callable, Iterator from http.cookies import BaseCookie, SimpleCookie +from types import MappingProxyType from typing import Any, cast from unittest import mock from uuid import uuid4 @@ -755,6 +756,14 @@ def unsafe(self) -> bool: def quote_cookie(self) -> bool: return True + @property + def cookies(self) -> MappingProxyType[tuple[str, str], SimpleCookie]: + return MappingProxyType({}) + + @property + def host_only_cookies(self) -> frozenset[tuple[str, str]]: + return frozenset() + def clear(self, predicate: abc.ClearCookiePredicate | None = None) -> None: self._clear_mock(predicate) @@ -777,6 +786,8 @@ def __iter__(self) -> Iterator[Any]: assert jar.quote_cookie is True assert jar.unsafe is False + assert jar.cookies == MappingProxyType({}) + assert jar.host_only_cookies == frozenset() assert len(jar) == 0 assert list(jar) == [] jar.clear() diff --git a/tests/test_cookiejar.py b/tests/test_cookiejar.py index d3001191fbd..6dac3a0adb4 100644 --- a/tests/test_cookiejar.py +++ b/tests/test_cookiejar.py @@ -12,6 +12,7 @@ from http.cookies import BaseCookie, Morsel, SimpleCookie from operator import not_ from pathlib import Path +from types import MappingProxyType from unittest import mock import pytest @@ -840,6 +841,54 @@ async def test_dummy_cookie_jar() -> None: dummy_jar.clear() +async def test_dummy_cookie_jar_cookies_property() -> None: + dummy_jar = DummyCookieJar() + assert dict(dummy_jar.cookies) == {} + assert dummy_jar.host_only_cookies == frozenset() + + +async def test_cookie_jar_cookies_property() -> None: + jar = CookieJar() + cookie = SimpleCookie( + "shared-cookie=first; domain-cookie=second; Domain=example.com; Path=/; " + ) + jar.update_cookies(cookie, URL("http://example.com/")) + + cookies = jar.cookies + # Should be a read-only view + assert isinstance(cookies, MappingProxyType) + # Should contain the stored cookies with their full attributes + found_names = {name for simple_cookie in cookies.values() for name in simple_cookie} + assert "shared-cookie" in found_names + assert "domain-cookie" in found_names + # Verify that domain attribute is preserved + for key, simple_cookie in cookies.items(): + for name, morsel in simple_cookie.items(): + if name == "domain-cookie": + assert morsel["domain"] == "example.com" + assert morsel["path"] == "/" + + +async def test_cookie_jar_host_only_cookies_property() -> None: + jar = CookieJar() + # Cookies without an explicit Domain attribute are host-only + cookie = SimpleCookie("hostonly=value;") + jar.update_cookies(cookie, URL("http://example.com/")) + + host_only = jar.host_only_cookies + assert isinstance(host_only, frozenset) + assert ("example.com", "hostonly") in host_only + + +async def test_cookie_jar_cookies_property_immutable() -> None: + jar = CookieJar() + cookie = SimpleCookie("foo=bar;") + jar.update_cookies(cookie, URL("http://example.com/")) + cookies = jar.cookies + with pytest.raises(TypeError): + cookies[("new", "key")] = SimpleCookie() # type: ignore[index] + + async def test_loose_cookies_types() -> None: jar = CookieJar() From 56562d025cd5dea41e213735b4892cce8fdaa5b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 17:10:45 +0000 Subject: [PATCH 142/191] Bump aiodns from 4.0.3 to 4.0.4 (#12663) Bumps [aiodns](https://github.com/aio-libs/aiodns) from 4.0.3 to 4.0.4.
Changelog

Sourced from aiodns's changelog.

4.0.4

  • Raise DNSError(ARES_ENODATA) from query() when the answer section has no records of the requested qtype, restoring the pycares 4.x NODATA contract and avoiding AttributeError for CNAME/SOA/PTR callers (#254).
  • Add the missing build-backend entry to pyproject.toml so PEP 517 builds from the sdist work without falling back to the deprecated legacy setuptools backend (#252).
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=aiodns&package-manager=pip&previous-version=4.0.3&new-version=4.0.4)](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/base-ft.txt | 2 +- requirements/base.txt | 2 +- requirements/constraints.txt | 6 ++---- requirements/dev.txt | 6 ++---- requirements/lint.txt | 2 +- requirements/runtime-deps.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 8 files changed, 10 insertions(+), 14 deletions(-) diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt index d15daa320f9..55b6049b556 100644 --- a/requirements/base-ft.txt +++ b/requirements/base-ft.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/base-ft.txt --strip-extras requirements/base-ft.in # -aiodns==4.0.3 +aiodns==4.0.4 # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.1 # via -r requirements/runtime-deps.in diff --git a/requirements/base.txt b/requirements/base.txt index eaa71549020..640c58a664d 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/base.txt --strip-extras requirements/base.in # -aiodns==4.0.3 +aiodns==4.0.4 # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.1 # via -r requirements/runtime-deps.in diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 7187b799f9f..9b1c4426589 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/constraints.txt --strip-extras requirements/constraints.in # -aiodns==4.0.3 +aiodns==4.0.4 # via # -r requirements/lint.in # -r requirements/runtime-deps.in @@ -25,9 +25,7 @@ async-timeout==5.0.1 ; python_version < "3.11" # -r requirements/runtime-deps.in # valkey attrs==26.1.0 - # via - # -r requirements/runtime-deps.in - # myst-parser + # via -r requirements/runtime-deps.in babel==2.18.0 # via sphinx backports-zstd==1.3.0 ; implementation_name == "cpython" and python_version < "3.14" diff --git a/requirements/dev.txt b/requirements/dev.txt index fbc7bcf0649..b4b7a5d52f3 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/dev.txt --strip-extras requirements/dev.in # -aiodns==4.0.3 +aiodns==4.0.4 # via # -r requirements/lint.in # -r requirements/runtime-deps.in @@ -25,9 +25,7 @@ async-timeout==5.0.1 ; python_version < "3.11" # -r requirements/runtime-deps.in # valkey attrs==26.1.0 - # via - # -r requirements/runtime-deps.in - # myst-parser + # via -r requirements/runtime-deps.in babel==2.18.0 # via sphinx backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" diff --git a/requirements/lint.txt b/requirements/lint.txt index ea061166130..c212712be39 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/lint.txt --resolver=backtracking --strip-extras requirements/lint.in # -aiodns==4.0.3 +aiodns==4.0.4 # via -r requirements/lint.in annotated-types==0.7.0 # via pydantic diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index 549c5576f4d..8d2d7508455 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/runtime-deps.txt --strip-extras requirements/runtime-deps.in # -aiodns==4.0.3 +aiodns==4.0.4 # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.1 # via -r requirements/runtime-deps.in diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index a103d39b26b..86a2d7bf908 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/test-ft.txt --strip-extras requirements/test-ft.in # -aiodns==4.0.3 +aiodns==4.0.4 # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.1 # via -r requirements/runtime-deps.in diff --git a/requirements/test.txt b/requirements/test.txt index 6449ddcfb14..6237c964ef8 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/test.txt --strip-extras requirements/test.in # -aiodns==4.0.3 +aiodns==4.0.4 # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.1 # via -r requirements/runtime-deps.in From 40a4ddcdb140dd85b82da16a1b4305c7a4f472f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 17:29:10 +0000 Subject: [PATCH 143/191] Bump certifi from 2026.4.22 to 2026.5.20 (#12668) Bumps [certifi](https://github.com/certifi/python-certifi) from 2026.4.22 to 2026.5.20.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=certifi&package-manager=pip&previous-version=2026.4.22&new-version=2026.5.20)](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/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 9b1c4426589..c4bb0a97c8a 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -40,7 +40,7 @@ brotli==1.2.0 ; platform_python_implementation == "CPython" # via -r requirements/runtime-deps.in build==1.5.0 # via pip-tools -certifi==2026.4.22 +certifi==2026.5.20 # via requests cffi==2.0.0 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index b4b7a5d52f3..6a33929dfae 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -40,7 +40,7 @@ brotli==1.2.0 ; platform_python_implementation == "CPython" # via -r requirements/runtime-deps.in build==1.5.0 # via pip-tools -certifi==2026.4.22 +certifi==2026.5.20 # via requests cffi==2.0.0 # via diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index e653ba3adea..b196c4967d9 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -10,7 +10,7 @@ alabaster==1.0.0 # via sphinx babel==2.18.0 # via sphinx -certifi==2026.4.22 +certifi==2026.5.20 # via requests charset-normalizer==3.4.7 # via requests diff --git a/requirements/doc.txt b/requirements/doc.txt index 8466dd55845..8ae2412c091 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -10,7 +10,7 @@ alabaster==1.0.0 # via sphinx babel==2.18.0 # via sphinx -certifi==2026.4.22 +certifi==2026.5.20 # via requests charset-normalizer==3.4.7 # via requests From 1401ebd0c5a666a39ceddcb84b1fa3cadd36f047 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 17:50:28 +0000 Subject: [PATCH 144/191] Bump aiohappyeyeballs from 2.6.1 to 2.6.2 (#12665) Bumps [aiohappyeyeballs](https://github.com/aio-libs/aiohappyeyeballs) from 2.6.1 to 2.6.2.
Release notes

Sourced from aiohappyeyeballs's releases.

v2.6.2 (2026-05-20)

Bug Fixes

  • Clear error on empty addr_infos in start_connection (#222, 55dd76a)

Refactoring

  • Optimize obtaining event-loop down to 1 line (#185, 02a7029)

Testing

  • Stop verify_no_lingering_tasks from leaking an event loop (#221, ce43beb)

Detailed Changes: v2.6.1...v2.6.2

Changelog

Sourced from aiohappyeyeballs's changelog.

v2.6.2 (2026-05-20)

Bug fixes

  • Clear error on empty addr_infos in start_connection (55dd76a)

Testing

  • Stop verify_no_lingering_tasks from leaking an event loop (ce43beb)

Refactoring

  • Optimize obtaining event-loop down to 1 line (02a7029)
Commits
  • d779d62 2.6.2
  • 55dd76a fix: clear error on empty addr_infos in start_connection (#222)
  • ce43beb test: stop verify_no_lingering_tasks from leaking an event loop (#221)
  • 184079b chore(deps-ci): bump the github-actions group across 1 directory with 7 updat...
  • d9b0fa6 chore(deps-dev): bump requests from 2.32.4 to 2.33.0 (#219)
  • 09dcc92 chore(deps-dev): bump urllib3 from 2.5.0 to 2.7.0 (#220)
  • a0a313a chore(deps-dev): bump cryptography from 43.0.3 to 46.0.7 (#216)
  • b5dec0c ci: replace per-commit conventional commits check with pr title check (#218)
  • 363b722 chore(pre-commit.ci): pre-commit autoupdate (#202)
  • cc07391 chore: drop Python 3.9 support (#215)
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/base-ft.txt | 2 +- requirements/base.txt | 2 +- requirements/constraints.txt | 2 +- requirements/dev.txt | 2 +- requirements/runtime-deps.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt index 55b6049b556..2592806157c 100644 --- a/requirements/base-ft.txt +++ b/requirements/base-ft.txt @@ -6,7 +6,7 @@ # aiodns==4.0.4 # via -r requirements/runtime-deps.in -aiohappyeyeballs==2.6.1 +aiohappyeyeballs==2.6.2 # via -r requirements/runtime-deps.in aiosignal==1.4.0 # via -r requirements/runtime-deps.in diff --git a/requirements/base.txt b/requirements/base.txt index 640c58a664d..e3c17b4f01d 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -6,7 +6,7 @@ # aiodns==4.0.4 # via -r requirements/runtime-deps.in -aiohappyeyeballs==2.6.1 +aiohappyeyeballs==2.6.2 # via -r requirements/runtime-deps.in aiosignal==1.4.0 # via -r requirements/runtime-deps.in diff --git a/requirements/constraints.txt b/requirements/constraints.txt index c4bb0a97c8a..bb974de91b7 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -8,7 +8,7 @@ aiodns==4.0.4 # via # -r requirements/lint.in # -r requirements/runtime-deps.in -aiohappyeyeballs==2.6.1 +aiohappyeyeballs==2.6.2 # via -r requirements/runtime-deps.in aiohttp-theme==0.1.7 # via -r requirements/doc.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 6a33929dfae..9a67a45909e 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -8,7 +8,7 @@ aiodns==4.0.4 # via # -r requirements/lint.in # -r requirements/runtime-deps.in -aiohappyeyeballs==2.6.1 +aiohappyeyeballs==2.6.2 # via -r requirements/runtime-deps.in aiohttp-theme==0.1.7 # via -r requirements/doc.in diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index 8d2d7508455..d83995a61af 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -6,7 +6,7 @@ # aiodns==4.0.4 # via -r requirements/runtime-deps.in -aiohappyeyeballs==2.6.1 +aiohappyeyeballs==2.6.2 # via -r requirements/runtime-deps.in aiosignal==1.4.0 # via -r requirements/runtime-deps.in diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 86a2d7bf908..825a2767520 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -6,7 +6,7 @@ # aiodns==4.0.4 # via -r requirements/runtime-deps.in -aiohappyeyeballs==2.6.1 +aiohappyeyeballs==2.6.2 # via -r requirements/runtime-deps.in aiosignal==1.4.0 # via -r requirements/runtime-deps.in diff --git a/requirements/test.txt b/requirements/test.txt index 6237c964ef8..9c51a06bba0 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -6,7 +6,7 @@ # aiodns==4.0.4 # via -r requirements/runtime-deps.in -aiohappyeyeballs==2.6.1 +aiohappyeyeballs==2.6.2 # via -r requirements/runtime-deps.in aiosignal==1.4.0 # via -r requirements/runtime-deps.in From 677797fb1d6e3e37ab3c5829baf4f7c2bc3dc620 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 21:45:58 +0100 Subject: [PATCH 145/191] [PR #7049/cbf25531 backport][3.14] add full apache license (#12675) **This is a backport of PR #7049 as merged into master (cbf255319921ba84a115d123f7304f9f813e1139).** Co-authored-by: Lukas Kahwe Smith --- LICENSE.txt | 190 +++++++++++++++++++++++++++++++++++++++++++++++++++- README.rst | 5 -- 2 files changed, 189 insertions(+), 6 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index e497a322f20..0b2f7b0655f 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,192 @@ - Copyright aio-libs contributors. +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright aio-libs contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.rst b/README.rst index e6f428640da..d5c8dd835bc 100644 --- a/README.rst +++ b/README.rst @@ -171,11 +171,6 @@ Optionally you may install the aiodns_ library (highly recommended for sake of s .. _yarl: https://pypi.python.org/pypi/yarl .. _async-timeout: https://pypi.python.org/pypi/async_timeout -License -======= - -``aiohttp`` is offered under the Apache 2 license. - Keepsafe ======== From 18be3b9abe71fe1fde99596ba2cdfe3612960529 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 00:40:57 +0100 Subject: [PATCH 146/191] [PR #12551/feb0405a backport][3.14] Threat model chapter 2 (#12676) **This is a backport of PR #12551 as merged into master (feb0405a8e93a016cf352a9adbdf6487ae1b6b92).** Co-authored-by: Sam Bull --- THREAT_MODEL.md | 133 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 126 insertions(+), 7 deletions(-) diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index 3f48765e249..cb50d264489 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -6,6 +6,11 @@ intended to (a) make explicit the implicit security assumptions baked into the codebase, (b) catalogue known classes of threat against each subsystem, and (c) record the existing and recommended mitigations. +Some mitigations are expected to be in the application code built on top +of aiohttp. Recommendations addressed to application authors rather than +to aiohttp maintainers are prefixed **User:** to make the responsibility +explicit. + --- ## 1. Library Overview @@ -255,14 +260,14 @@ into `StreamReader`) is then handed to `web_protocol.RequestHandler` and | 1.1 | Smuggling via duplicate framing headers | llhttp rejects conflicting `Content-Length`. `http_parser.py:HttpRequestParserPy.parse_headers` rejects coexistence of CL + `Transfer-Encoding: chunked`. The full `SINGLETON_HEADERS` set (CL, CT, Host, TE, ETag, etc.) is duplicate-rejected by the request parser (strict mode); `#12302` disabled this check on the response parser (lax mode), since real-world servers commonly send duplicate `Content-Type` / `Server`. | If new singleton-sensitive headers emerge in HTTP/1.1 RFC errata, add to `SINGLETON_HEADERS`. | | 1.2 | Lenient response parsing | Lenient flags (`llhttp_set_lenient_headers`, `llhttp_set_lenient_optional_cr_before_lf`, `llhttp_set_lenient_spaces_after_chunk_size`) are only enabled on the **response** parser and only when `DEBUG` is False (set in `HttpResponseParser.__init__`). The **request** parser is strict. | Documented design decision: keep lenient response parsing for real-world server interop | | 1.3 | CRLF / NUL in header values | Bytes `\r`, `\n`, `\x00` rejected in header values (`_http_parser.pyx` callbacks; `http_parser.py:HeadersParser.parse_headers`). | Keep regression tests in `tests/test_http_parser.py` covering each forbidden byte both in name and value, and across both Cython and pure-Python parsers. | -| 1.4 | Non-UTF-8 round-trip | None at parser layer (intentional — preserving original bytes is required for some use cases). | **Document in user-facing docs that header values are bytes-preserving; warn against reflecting headers verbatim into responses, logs, or sub-requests without re-validation.** | +| 1.4 | Non-UTF-8 round-trip | None at parser layer (intentional — preserving original bytes is required for some use cases). | **Document in user-facing docs that header values are bytes-preserving.** **User**: Re-validate any header value before reflecting it into responses, logs, or sub-requests. | | 1.5 | HTTP version regex accepts 0.9 / 2.0 | None (regex is permissive). | **Tighten `VERSRE` (and llhttp configuration if possible) to reject anything outside `HTTP/1.0` and `HTTP/1.1`.** | -| 1.6 | Method-case round-trip | Method token validated by regex; not canonicalised. | **Document that user route handlers / authorization checks should compare methods case-sensitively to the canonical RFC tokens, or use the framework's `web.RouteTableDef` decorators which already match canonical methods.** | +| 1.6 | Method-case round-trip | Method token validated by regex; not canonicalised. | **Document the asymmetry.** **User**: Compare HTTP methods case-sensitively to canonical RFC tokens, or use `web.RouteTableDef` decorators (which already match canonical methods). | | 1.7 | `Content-Length` parsing | llhttp validates CL is decimal and non-negative; pure-Python parser validates via `DIGITS.fullmatch(r"\d+")` before `int(...)`, rejecting `+`/`-`/non-ASCII-digit forms (`test_bad_headers`, `test_headers_content_length_err_*` cover these). | None. Cross-backend parity is covered by the shared parser tests. | | 1.8 | `Transfer-Encoding` lenience | `_is_chunked_te` requires `chunked` to be the last value; duplicate `chunked` rejected (`#10611`). Request parser strict. | None. | | 1.9 | Chunk-size DoS | The parser doesn't cap chunk size, but **server-side body length is bounded by `client_max_size` (default `1 MiB`)** in `web_request.py:BaseRequest.read`. Client-side responses are bounded by user-supplied `max_body_size` / streaming reads. | None. If a cap is ever needed at the parser level, plumb it through `HttpPayloadParser`. | | 1.10 | Chunk-extension DoS | Chunk-extension content is bounded by the same wire-level size constraints (it shares the chunk-size line with `max_line_size`). | **Add an explicit test that chunk-extension flooding cannot blow past `max_line_size`.** | -| 1.11 | Parser error reflection | `http_parser.py` truncates to `[:100]` for line errors. Server-side error path renders 4xx with the exception message; tracebacks only when `DEBUG=True`. | **Audit any path where `BadHttpMessage` content is reflected to the client unsanitised (especially in custom `web_log` configurations).** | +| 1.11 | Parser error reflection | `http_parser.py` truncates to `[:100]` for line errors. Server-side error path renders 4xx with the exception message; tracebacks only when `DEBUG=True`. | **Audit any aiohttp path where `BadHttpMessage` content is reflected to the client unsanitised.** **User**: Review custom `web_log` configurations and any middleware that reflects parser exception messages back to the peer. | | 1.12 | Cython ⇄ pure-Python divergence | `tests/test_http_parser.py` parameterises tests over `REQUEST_PARSERS` / `RESPONSE_PARSERS` (pure-Python always; Cython when the extension imports). The high-leverage attack vectors are already covered under both backends: CL+TE (`test_content_length_transfer_encoding`), CL×N (`test_duplicate_singleton_header_rejected`), obs-fold (`test_reject_obsolete_line_folding`, `test_http_response_parser_obs_line_folding*`), CR/LF/NUL (`test_bad_headers`, `test_http_response_parser_null_byte_in_header_value`, `test_http_response_parser_bad_crlf`), version regex (`test_http_request_parser_bad_version*`, `test_http_response_parser_bad_version*`). | None. When new attack vectors emerge, add them to the parameterised tests. | | 1.13 | llhttp version drift | Manual upgrade via `make generate-llhttp`; vendor pinned in `vendor/llhttp/package.json`. | Track upstream releases (e.g. via Dependabot rule for `vendor/llhttp/package.json`), bump on every llhttp release, regenerate in CI. | | 1.14 | npm-side compromise of `llhttp` | The vendored output is checked into git, so a compromise during a future regen would be detectable in PR review. See [§5.19](#519-build--release-supply-chain). | **Make the llhttp build reproducible: pin Node.js version, commit the npm lockfile, and on every bump verify the regenerated C against upstream's release tarballs before committing.** | @@ -272,12 +277,12 @@ into `StreamReader`) is then handed to `web_protocol.RequestHandler` and - **GHSA-xx9p-xxvh-7g8j (CVE-2023-47641)** (3.8.0) — CL-vs-TE divergence between the Cython and pure-Python parsers, allowing request smuggling against deployments that switched backends. -- **CVE-2023-37276 / GHSA-45c4-8wx5-qw6w** (3.8.5) — HTTP request smuggling +- **GHSA-45c4-8wx5-qw6w (CVE-2023-37276)** (3.8.5) — HTTP request smuggling via CR/LF/NUL in header values. Both parsers reject these bytes at the byte level. - **GHSA-pjjw-qhg8-p2p9** (3.8.6) — smuggling pair in vendored llhttp 8.1.1; fixed by bumping llhttp to 9. -- **GHSA-gfw2-4jvh-wgfg / GHSA-8qpw-xqxj-h4r2** (3.8.6 / 3.9.2) — pure-Python +- **GHSA-gfw2-4jvh-wgfg (CVE-2023-47627) / GHSA-8qpw-xqxj-h4r2 (CVE-2024-23829)** (3.8.6 / 3.9.2) — pure-Python parser accepted lenient separators / weak RFC validation that llhttp rejected. - **GHSA-8495-4g3g-x7pr (CVE-2024-52304)** (3.10.11) — chunk-extension @@ -287,12 +292,12 @@ into `StreamReader`) is then handed to `web_protocol.RequestHandler` and - **GHSA-69f9-5gxw-wvc2 (CVE-2025-69224)** (3.13.3) — Unicode codepoints matched by `\d` in the pure-Python parser's regexes were treated as digits. -- **GHSA-g84x-mcqj-x9qq** (3.13.3) — CPU-DoS on `request.read()` when +- **GHSA-g84x-mcqj-x9qq (CVE-2025-69229)** (3.13.3) — CPU-DoS on `request.read()` when the body arrives as a very large number of small chunks. - **PR #12137** (3.13.4) — precautionary hardening: pure-Python parser now explicitly rejects duplicate `Transfer-Encoding: chunked` on the request parser. -- **GHSA-c427-h43c-vf67** (3.13.4) — duplicate `Host` header accepted +- **GHSA-c427-h43c-vf67 (CVE-2026-34525)** (3.13.4) — duplicate `Host` header accepted in request parser, bypassing `Application.add_domain()` host-based routing / authorisation. Fixed by adding `Host` to the strict request-parser singleton rejection set. @@ -310,3 +315,117 @@ into `StreamReader`) is then handed to `web_protocol.RequestHandler` and request parser (strict). These are all currently in place; this section assumes no regression. + +--- + +### 5.2. HTTP/1 writer + +**Scope.** Serialisation of outbound HTTP/1.x messages — request lines, status +lines, header blocks, chunked / fixed-length / EOF-terminated bodies, drain / +backpressure behaviour. Both server-side response emission and client-side +request emission share the same `StreamWriter`. Out of scope: WebSocket frame +emission ([§5.3](#53-websocket-framing--per-message-deflate)), payload generation for multipart ([§5.4](#54-multipart-parsing--encoding)), compression codecs +([§5.5](#55-compression-codecs)), the user-handler-facing parts of `web.Response` and +`ClientRequest` (covered in [§5.9](#59-server-requestresponse-objects) and [§5.12](#512-client-api--request-lifecycle) respectively, but +called out where the writer's safety depends on them). + +**Components covered.** + +- `aiohttp/_http_writer.pyx` — Cython `_serialize_headers` and + `_write_str_raise_on_nlcr` (the CR/LF/NUL bytewise rejector). +- `aiohttp/http_writer.py` — `StreamWriter` (the `AbstractStreamWriter` + implementation) plus the pure-Python `_py_serialize_headers` / + `_safe_header` fallback and the Cython/pure-Python switch at + `http_writer.py:_py_serialize_headers`. +- `aiohttp/abc.py` — `AbstractStreamWriter` interface. +- Header-source feeders: `aiohttp/web_response.py` (server), + `aiohttp/client_reqrep.py` (client), `aiohttp/helpers.py:populate_with_cookies`. + +**Selection.** `_serialize_headers` defaults to the pure-Python +implementation; if `_http_writer` (Cython) imports successfully and +`AIOHTTP_NO_EXTENSIONS` is unset, the Cython implementation replaces it +(`http_writer.py:_py_serialize_headers`). Both implementations apply the same +CR / LF / NUL rejection on names *and* values *and* the status/request line. + +**Trust boundaries & data flow.** + +```mermaid +flowchart LR + Handler([User handler / ClientRequest]) -->|status_line, headers, body| SW[StreamWriter] + SW --> Serialize[_serialize_headers] + Serialize -->|reject CR/LF/NUL| Bytes[Wire bytes] + SW --> Body[write / write_eof / write_chunked] + Body --> Bytes + Bytes --> Transport[(asyncio Transport)] +``` + +The writer's input is **trusted** in the threat-model sense — i.e., it comes +from in-process Python code that ran the user's handler or constructed the +client request. The writer's job is therefore **structural integrity**: ensure +that whatever bytes a handler attempts to emit cannot escape the framing of a +single HTTP message and inject new headers, new status lines, or new requests +on the wire. The wire-side consumer is the **untrusted** counterparty +(arbitrary peer or intermediary). + +**Assets at risk (chunk-specific).** + +- **Outbound framing integrity** — one logical message ↔ one well-framed wire + message; no smuggling on the egress side. +- **Header integrity** — no name/value can introduce additional headers, + status lines, or chunk markers. +- **Liveness of the connection** — a slow / hostile reader cannot drive the + server (or client) into unbounded memory growth via writer buffering. + +**Threats (STRIDE).** + +| # | Component / Vector | STRIDE | Threat | Risk | +| :--- | :--- | :--- | :--- | :--- | +| 2.1 | Header name/value with CR / LF / NUL | T / I | Response-splitting / header injection allowing the next "header" or even a complete second response/request to be appended on the wire. | High | +| 2.2 | Status-line `reason` with CR / LF | T | Same family as 2.1 but on the status line; could let an attacker-controlled reason inject a body or a second status line. | High | +| 2.3 | Request-line path/method | T | Path-side smuggling via CR / LF / NUL or whitespace inside the path the writer emits. | Medium | +| 2.4 | `Content-Length` ≠ actual body length | T | If a handler / ClientRequest emits a body whose length disagrees with declared `Content-Length`, an intermediary may interpret framing differently from the writer's peer (smuggling). | Medium | +| 2.5 | `Content-Length` *and* `Transfer-Encoding: chunked` | T | Both headers reach the wire if user code constructs them via the raw headers dict; intermediaries disagree on which wins. | Medium | +| 2.6 | Body emission on HEAD / 1xx / 204 / 304 | T | Writer strips CL/TE for empty-body responses but **does not block the application from writing a body**; bytes after the `\r\n\r\n` confuse the next pipelined request. | Medium | +| 2.7 | `Set-Cookie` / `Cookie` value | T | Cookie name or value containing CR / LF / NUL passes through `SimpleCookie.output()` unchanged; only caught by writer's header validation. | Medium | +| 2.8 | Compression / `Content-Encoding` | T | Body double-compression when user sets `Content-Encoding` manually and also enables `compress=...`. Intermediaries may reject or mis-decode a doubly-compressed body. | Low | +| 2.9 | Drain / backpressure on slow readers | D | Slow consumer (or `Sec-WebSocket-Key`-style hold) keeps `transport.write()` queued; writer drains at 64 KiB threshold (`http_writer.py:StreamWriter.write`). A handler that doesn't await `drain()` can blow up. | Medium | +| 2.10 | Single oversized chunk | D | `write(b)` with a multi-GB blob is handed straight to `transport.write`; memory pressure shifts to asyncio's buffer. | Low | +| 2.11 | Chunked encoding hex framing | T | Malformed chunk-size lines (negative values, leading-`+`, leading zeros, hex obfuscation) would let a non-aiohttp peer reframe the body differently and smuggle. | Low | +| 2.12 | Header insertion validation timing | T | CR/LF/NUL rejection is *write-time*, not *insert-time*. A handler that sets a malicious header and then aborts before `write_headers()` will not raise. (Documented; not a recommended change.) | Low | +| 2.13 | Cython ⇄ pure-Python parity | T | Divergence between the two `_serialize_headers` implementations could let one backend silently pass CR/LF/NUL that the other rejects, weakening egress safety asymmetrically. | Low | +| 2.14 | Trailers asymmetry | T | The writer never emits trailers, but the parser accepts incoming trailers; not a writer-side threat in itself, just a documentation point for completeness. | Low | + +**Mitigations.** + +| # | Threat | Existing | Recommended | +| :--- | :--- | :--- | :--- | +| 2.1 | Header CR / LF / NUL injection | Both backends reject these bytes via `_write_str_raise_on_nlcr` (`_http_writer.pyx:_write_str_raise_on_nlcr`) and `_safe_header` (`http_writer.py:_safe_header`), raising `ValueError` from `_serialize_headers` before any byte hits the transport. Applied symmetrically to names, values, and the status line. | **The current tests import whichever `_serialize_headers` won the import, so only one backend is exercised. Parameterise like `tests/test_http_parser.py` does (cross-cuts [§6.1](#61-highest-leverage-recommendations) #3).** | +| 2.2 | Status-line `reason` injection | `web_response.Response._set_status` (`web_response.py:StreamResponse._set_status`) rejects `\r` / `\n` in `reason` *at set-time*. The writer also rejects them at write-time as part of the status-line validation. | None. | +| 2.3 | Request-line path / method | The full status line (`{method} {path} HTTP/{v}.{v}`) goes through `_write_str_raise_on_nlcr` / `_safe_header`, so CR / LF / NUL are caught regardless of whether `path` came from `yarl` or `method` was a caller-supplied string. yarl additionally rejects these bytes earlier per RFC 3986. | None. | +| 2.4 | CL / body-length mismatch | None at write-time. `web.Response.write_eof` and the chunked writer write what they're given. | **Recommended hardening: in DEBUG mode, assert / warn when actual bytes-written disagrees with declared `Content-Length` at `write_eof()`. Useful for catching smuggling-adjacent bugs in user handlers.** | +| 2.5 | CL + TE simultaneous | Server-side `enable_chunked_encoding()` (`web_response.py:StreamResponse.enable_chunked_encoding`) raises if `Content-Length` is already set; client-side `_update_transfer_encoding()` (`client_reqrep.py:ClientRequest._update_transfer_encoding`) raises if user sets `chunked=True` while `Content-Length` is in headers. Manual user injection into the raw headers dict is *not* caught. | **Consider a write-time assert in `StreamWriter` that rejects `Content-Length` and `Transfer-Encoding: chunked` coexisting.** | +| 2.6 | Body-suppression edge cases | `web_response.py:StreamResponse._prepare_headers` strips `Content-Length` and `Transfer-Encoding` for HEAD / 1xx / 204 / 304 (`EMPTY_BODY_STATUS_CODES`, `helpers.py:EMPTY_BODY_METHODS`). The framework's own machinery doesn't write a body for these. | **User**: Do not call `resp.write(...)` in a handler responding HEAD / 1xx / 204 / 304 — framing strips CL / TE but does not block the byte write. **Optional aiohttp change: have `StreamWriter` short-circuit body writes when `length == 0` and the response was framed as empty-body.** | +| 2.7 | Cookie injection | `populate_with_cookies` (`helpers.py:populate_with_cookies`) routes the cookie through `SimpleCookie.output()` and then into a regular header, where the CR / LF / NUL check at write-time catches anything `SimpleCookie` happened to pass through. | Documented design decision: rely on writer-level validation rather than tightening `set_cookie` / `populate_with_cookies` further. Keep regression tests covering cookie name/value with CR / LF / NUL across both backends. | +| 2.8 | Manual `Content-Encoding` | Server side: `enable_compression()` (`web_response.py:StreamResponse.enable_compression`) returns early if `Content-Encoding` already present, so the body is not double-compressed. Client side: `ClientRequest._update_content_encoding` raises `ValueError("compress can not be set if Content-Encoding header is set")` — symmetric guard. | None. | +| 2.9 | Drain / backpressure | `StreamWriter.write` drains at `LIMIT = 0x10000` bytes (`http_writer.py:StreamWriter.write`) when `drain=True` is set by the caller. Application code is expected to `await write(...)` to honour backpressure. | **User**: `await write(...)` in handlers; tight `for` loops without `await` can starve the event loop. Cross-reference [§5.7](#57-server-connection-lifecycle) for connection-level read/write timeouts that mitigate slow consumers. | +| 2.10 | Oversized single chunk | None at the writer layer — bytes go straight to `transport.write`. asyncio applies its own high-water marks via the transport. | **User**: Relies on application-level bounds (use streaming, generators, `FileResponse`, etc., for large bodies). | +| 2.11 | Chunked hex framing | The writer always uses `f"{len(chunk):x}\r\n"` followed by the chunk and `\r\n` (`http_writer.py:StreamWriter._write_chunked_payload`). | None. | +| 2.12 | Insert-time vs write-time validation | Headers are validated at write-time only; `set_status` validates `reason` at set-time. | Documented design decision: late validation is acceptable; keep behaviour as-is. | +| 2.13 | Cython ⇄ pure-Python parity | Both backends share the same logic and test surface; the Cython version uses a fast bytewise check, the Python version uses `in` on three sentinel characters. | **Parameterise the writer tests over both backends so egress equivalence on malicious inputs is exercised under both (see [§6.1](#61-highest-leverage-recommendations) #3).** | +| 2.14 | Trailers asymmetry | Writer does not emit trailers; parser accepts trailers on incoming. Documented for completeness. | None. | + +**Past advisories / hardening (recap).** + +- **GHSA-q3qx-c6g2-7pw2 (CVE-2023-49081)** (3.9.0) — `ClientSession` + CRLF injection via the HTTP `version` argument. +- **GHSA-qvrw-v9rv-5rjx (CVE-2023-49082)** (3.9.0) — `ClientSession` CRLF injection via + the `method` argument (request-line injection). +- **GHSA-mwh4-6h8g-pg8w (CVE-2026-34519)** (3.13.4) — response-splitting via `\r` in + the status-line `reason`. Fixed by rejecting CR/LF in `reason` at + `_set_status` set-time, on top of the existing writer-side check + (threat 2.2). + +Writer-level CR / LF / NUL rejection via `_safe_header` and +`_write_str_raise_on_nlcr` has been in place since the header-injection +family of issues was first surfaced (well before CVE-2023-37276, which +was a parser-side fix). From ad6dbf89b4c7541fb406385ca62b1760da714e8e Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 02:43:54 +0100 Subject: [PATCH 147/191] [PR #12678/b9280223 backport][3.14] Submit llhttp for Github SBOM (#12679) **This is a backport of PR #12678 as merged into master (b92802234d84d98e617cd42a9862cd8697f2f571).** Co-authored-by: Sam Bull --- .github/workflows/dependency-submission.yml | 78 +++++++++++++++++++++ CHANGES/12678.packaging.rst | 1 + 2 files changed, 79 insertions(+) create mode 100644 .github/workflows/dependency-submission.yml create mode 100644 CHANGES/12678.packaging.rst diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml new file mode 100644 index 00000000000..323a5971cb9 --- /dev/null +++ b/.github/workflows/dependency-submission.yml @@ -0,0 +1,78 @@ +name: Dependency submission + +# GitHub's automatically generated dependency graph (and the SBOM exported from +# it) only covers dependencies declared in recognised package manifests. llhttp +# is vendored as a git submodule under vendor/llhttp, so that scan never sees +# it. This workflow submits llhttp explicitly through the Dependency submission +# API so it shows up in the dependency graph, the exported SBOM and Dependabot +# alerts. + +on: + push: + branches: + - master + paths: + - .gitmodules + - vendor/llhttp + - .github/workflows/dependency-submission.yml + +permissions: {} + +jobs: + llhttp: + name: Submit vendored llhttp + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: write # Required by the Dependency submission API + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: true + - name: Read vendored llhttp version + id: llhttp + run: echo "version=$(jq -re .version vendor/llhttp/package.json)" >> "$GITHUB_OUTPUT" + - name: Submit llhttp to the dependency graph + uses: actions/github-script@v9 + env: + LLHTTP_VERSION: ${{ steps.llhttp.outputs.version }} + with: + script: | + const version = process.env.LLHTTP_VERSION; + const response = await github.request( + 'POST /repos/{owner}/{repo}/dependency-graph/snapshots', + { + owner: context.repo.owner, + repo: context.repo.repo, + version: 0, + sha: context.sha, + ref: context.ref, + job: { + correlator: context.workflow, + id: context.runId.toString(), + }, + detector: { + name: 'aiohttp vendored dependency submission', + version: '1.0.0', + url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}`, + }, + scanned: new Date().toISOString(), + manifests: { + 'vendor/llhttp': { + name: 'vendor/llhttp', + file: { + source_location: '.gitmodules', + }, + resolved: { + llhttp: { + package_url: `pkg:npm/llhttp@${version}`, + relationship: 'direct', + scope: 'runtime', + }, + }, + }, + }, + }, + ); + core.info(`Submitted llhttp ${version}: ${response.data.message}`); diff --git a/CHANGES/12678.packaging.rst b/CHANGES/12678.packaging.rst new file mode 100644 index 00000000000..7e459cf9ff5 --- /dev/null +++ b/CHANGES/12678.packaging.rst @@ -0,0 +1 @@ +Submitted vendored `llhttp` to Github's SBOM -- by :user:`Dreamsorcerer`. From 9e703c735e1a387d3a06802714ed666d003a596b Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 03:23:43 +0100 Subject: [PATCH 148/191] [PR #12680/e37ab8be backport][3.14] Drop paths restriction on dep submission (#12682) **This is a backport of PR #12680 as merged into master (e37ab8be3cfc89a9ca6f9508428382630ce3d6b5).** Co-authored-by: Sam Bull --- .github/workflows/dependency-submission.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml index 323a5971cb9..9a499f22629 100644 --- a/.github/workflows/dependency-submission.yml +++ b/.github/workflows/dependency-submission.yml @@ -11,10 +11,6 @@ on: push: branches: - master - paths: - - .gitmodules - - vendor/llhttp - - .github/workflows/dependency-submission.yml permissions: {} From 3be82a043d6e013c1a7600ab12a93577a2508c53 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 10:59:26 +0000 Subject: [PATCH 149/191] Bump click from 8.4.0 to 8.4.1 (#12684) Bumps [click](https://github.com/pallets/click) from 8.4.0 to 8.4.1.
Release notes

Sourced from click's releases.

8.4.1

This is the Click 8.4.1 fix release, which fixes bugs but does not otherwise change behavior and should not result in breaking changes compared to the latest feature release.

PyPI: https://pypi.org/project/click/8.4.1/ Changes: https://click.palletsprojects.com/page/changes/#version-8-4-1 Milestone: https://github.com/pallets/click/milestone/32?closed=1

  • get_parameter_source() is available during eager callbacks and type conversion again. #3458 #3484
  • Zsh completion scripts parse correctly on Windows. #3277 # 3466
  • Shell completion of Choice Enum values produces a valid completion result. #3015
  • Fix empty byte-string handling in echo. #3487
  • Fix closed file error with echo_via_pager. #3449
Changelog

Sourced from click's changelog.

Version 8.4.1

Released 2026-05-21

  • get_parameter_source() is available during eager callbacks and type conversion again. :issue:3458 :issue:3484
  • Zsh completion scripts parse correctly on Windows. :issue:3277 :pr:3466
  • Shell completion of Choice Enum values produces a valid completion result. :issue:3015
  • Fix empty byte-string handling in echo. :issue:3487
  • Fix closed file error with echo_via_pager. :issue:3449
Commits
  • 6eeb50e release version 8.4.1
  • 67921d5 change log and doc fixes (#3495)
  • 9c41f46 Fix changelog and version admonitions
  • 6cb3477 fix skip condition
  • 5ee8e31 fix I/O operation on closed file error with CliRunner and echo_via_pager (#3482)
  • becbde5 pager doesn't close std streams
  • a5f5aa6 Handle empty bytes in echo (#3493)
  • 4d3db84 handle empty bytes in echo
  • d42f15b Fix get_parameter_source() during type conversion and eager callbacks (#3484)
  • 0baa8db Document ctx.params bypass with test and doc
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=click&package-manager=pip&previous-version=8.4.0&new-version=8.4.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 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 +- requirements/lint.txt | 2 +- requirements/test-common.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index bb974de91b7..fae88d992e5 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -50,7 +50,7 @@ cfgv==3.5.0 # via pre-commit charset-normalizer==3.4.7 # via requests -click==8.4.0 +click==8.4.1 # via # pip-tools # slotscheck diff --git a/requirements/dev.txt b/requirements/dev.txt index 9a67a45909e..5b16877d993 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -50,7 +50,7 @@ cfgv==3.5.0 # via pre-commit charset-normalizer==3.4.7 # via requests -click==8.4.0 +click==8.4.1 # via # pip-tools # slotscheck diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index b196c4967d9..8c92b8157cb 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -14,7 +14,7 @@ certifi==2026.5.20 # via requests charset-normalizer==3.4.7 # via requests -click==8.4.0 +click==8.4.1 # via towncrier docutils==0.21.2 # via diff --git a/requirements/doc.txt b/requirements/doc.txt index 8ae2412c091..8d6f78a500c 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -14,7 +14,7 @@ certifi==2026.5.20 # via requests charset-normalizer==3.4.7 # via requests -click==8.4.0 +click==8.4.1 # via towncrier docutils==0.21.2 # via diff --git a/requirements/lint.txt b/requirements/lint.txt index c212712be39..1d05336f9f5 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -22,7 +22,7 @@ cffi==2.0.0 # pycares cfgv==3.5.0 # via pre-commit -click==8.4.0 +click==8.4.1 # via slotscheck cryptography==48.0.0 # via trustme diff --git a/requirements/test-common.txt b/requirements/test-common.txt index dc964130606..ee87da7fc03 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -12,7 +12,7 @@ blockbuster==1.5.26 # via -r requirements/test-common.in cffi==2.0.0 # via cryptography -click==8.4.0 +click==8.4.1 # via wait-for-it coverage==7.14.0 # via diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 825a2767520..8385768b547 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -28,7 +28,7 @@ cffi==2.0.0 # via # cryptography # pycares -click==8.4.0 +click==8.4.1 # via wait-for-it coverage==7.14.0 # via diff --git a/requirements/test.txt b/requirements/test.txt index 9c51a06bba0..30587100a57 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -28,7 +28,7 @@ cffi==2.0.0 # via # cryptography # pycares -click==8.4.0 +click==8.4.1 # via wait-for-it coverage==7.14.0 # via From 9e6092a42a481580757f84789f1507e5fa131055 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 14:00:49 +0100 Subject: [PATCH 150/191] [PR #12681/1f005f12 backport][3.14] Update llhttp to v9.4.1 (#12683) **This is a backport of PR #12681 as merged into master (1f005f123956833b64d047fdc713138ae1893f56).** Co-authored-by: Sam Bull --- CHANGES/12681.packaging.rst | 1 + vendor/llhttp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 CHANGES/12681.packaging.rst diff --git a/CHANGES/12681.packaging.rst b/CHANGES/12681.packaging.rst new file mode 100644 index 00000000000..1cd12837ecb --- /dev/null +++ b/CHANGES/12681.packaging.rst @@ -0,0 +1 @@ +Updated ``llhttp`` to v9.4.1 -- by :user:`Dreamsorcerer`. diff --git a/vendor/llhttp b/vendor/llhttp index 06b12e87f20..96b15fb3bc0 160000 --- a/vendor/llhttp +++ b/vendor/llhttp @@ -1 +1 @@ -Subproject commit 06b12e87f209da43e3e9e0f958b7464a4a218896 +Subproject commit 96b15fb3bc00117d2db3df8e87fa6d3e9bcff328 From adb26e229fef4b83b569c7d17f038358f95079b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 16:41:10 +0000 Subject: [PATCH 151/191] Bump cython from 3.2.4 to 3.2.5 (#12694) Bumps [cython](https://github.com/cython/cython) from 3.2.4 to 3.2.5.
Changelog

Sourced from cython's changelog.

3.2.5 (2026-05-23)

Bugs fixed

  • A compile failure was fixed when using the walrus operator inside of try-except. (Github issue :issue:7462)

  • Expressions with side-effects as object argument to isinstance() could get evaluated multiple times, e.g. when they use the walrus operator. (Github issue :issue:7670)

  • Several problems generating the shared utility module were resolved, including a performance regression with memory views. (Github issues :issue:7487, :issue:7497, :issue:7504, :issue:7558)

  • Some GC and refcounting issues were resolved for Cython functions in the Limited API. (Github issue :issue:7594)

  • Refcounting errors and error handling issues were resolved in some rare error handling cases. (Github issues :issue:7597, :issue:7599, :issue:7612, :issue:7673)

  • Using cython.pymutex in an extension type with cdef methods generated invalid C code missing the required PyMutex declarations. (Github issue :issue:6995)

  • Calling .get_frame() on Cython coroutines could crash in freethreading Python. (Github issue :issue:7632)

  • The vectorcall protocol was not used correctly in .throw() of Cython coroutines when raising the exception only by type (without value or traceback). (Github issue :issue:7677)

  • A problem with cpdef enums in the Limited API of Python 3.11+ was resolved. (Github issue :issue:7503)

  • Unicode predicates like .isdigit() are now allowed to fail in the Limited API. (Github issue :issue:7602)

  • Conditional expressions mixing Python float and int object types could accidentally infer float as the common result type, instead of treating both independently.

  • Using sizeof() in the size declarations of extern arrays failed. (Github issue :issue:7451)

  • Enabling profiling generated invalid C code for non-Python return tuples. (Github issue :issue:7580)

  • abs() on C long long values could generate invalid C code.

... (truncated)

Commits
  • ec15209 Tests: Fix test in Py3.16, following cython/cython#7709
  • aa576c4 Fix test.
  • 2398ddd Prepare release of 3.2.5.
  • abb261f Update changelog.
  • a4bae70 Small cleanup of memoryview assertion (GH-7635)
  • 80d9e7e Prevent walrus operator from being re-evaluated multiple times in isinstance(...
  • 0c69532 CI: Pip PyPy 3.11 version to avoid CI failures.
  • f7d6b7a CI: Allow longer PyPy version strings than "major.minor".
  • b7a1d43 Update changelog.
  • f02df0a Build: Remove outdated license identifier (long replaced by license kw-opti...
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cython&package-manager=pip&previous-version=3.2.4&new-version=3.2.5)](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/cython.in | 2 +- requirements/cython.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index fae88d992e5..669e0ce1dab 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -62,7 +62,7 @@ coverage==7.14.0 # pytest-cov cryptography==48.0.0 # via trustme -cython==3.2.4 +cython==3.2.5 # via -r requirements/cython.in distlib==0.4.0 # via virtualenv diff --git a/requirements/cython.in b/requirements/cython.in index 6b848f6df9e..c8fcfa99220 100644 --- a/requirements/cython.in +++ b/requirements/cython.in @@ -1,3 +1,3 @@ -r multidict.in -Cython >= 3.1.1 +Cython >= 3.2.5 diff --git a/requirements/cython.txt b/requirements/cython.txt index 193a2299871..d6e2a89bf4d 100644 --- a/requirements/cython.txt +++ b/requirements/cython.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/cython.txt --resolver=backtracking --strip-extras requirements/cython.in # -cython==3.2.4 +cython==3.2.5 # via -r requirements/cython.in multidict==6.7.1 # via -r requirements/multidict.in From 20346dd60022c7a79fcf0486255a9f994323f751 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 17:01:09 +0000 Subject: [PATCH 152/191] Bump snowballstemmer from 3.0.1 to 3.1.0 (#12697) Bumps [snowballstemmer](https://github.com/snowballstem/snowball) from 3.0.1 to 3.1.0.
Changelog

Sourced from snowballstemmer's changelog.

Snowball 3.1.0 (2026-05-22)

Compiler changes

  • Bug fixes:

    • Fix segmentation fault if -syntax is used on a program with no code.

    • Fix segmentation fault on some assignment syntax errors.

    • Fix bug introduced in v3.0.0 with conversion of among starter. If there were any commands after the among in the same command list then the among itself would get lost. Not triggered by any current algorithms.

    • Clear name field when removing dead assignments. This is visible in the syntax tree shown when command line option -syntax is used, but probably doesn't affect anything otherwise.

  • Compiler command-line options:

    • Using - for the Snowball source file is now interpreted as stdin.

    • Improve comments generated by -comments to show more details of the corresponding Snowball code (e.g. variable names, arithmetic expressions, and literal strings).

    • Add -coverage option which enables a code coverage feature. So far this tracks which among strings and functions are exercised, and which grouping characters are exercised. !

    • Support -eprefix for all target languages. This is easy to do and provides a way to deal with externals which collide with keywords in the target language. Our build system now uses -eprefix _ for Python to make the stem external non-public (it is called by BaseStemmer method stemWord()) and we no longer hard-code prefixing Python externals with _.

    • Describe more options in --help output.

    • Sort target language options in --help output.

    • The -o option is now optional. If not specified we now write output(s) to the same filename as the first source, but with a different extension (e.g. path/to/english.sbl -> path/to/english.c and path/to/english.h).

    • The -o option can now optionally include an extension so you can now write -c++ -o path/to/foo.cxx instead of -c++ -o path/to/foo, which can be more convenient (e.g. in make rules) and also provides an easy way to

... (truncated)

Commits
  • 77e07c9 Update for 3.1.0
  • 4d37f9c Finalise NEWS entry for 3.1.0
  • 2d38e20 make update_version now also updates README.rst
  • 1596c74 Go: Fix code generated for non-constant hop
  • 34d1214 NEWS: Update draft entry
  • 65885b5 finnish: Rename things to match algo description
  • 5c05f53 finnish: Accept apostrophe instead of VI
  • df25742 JS: Generate simpler code for hop by constant
  • 27c9355 Add runtime test of hop/next
  • 2508d20 Add test coverage for hop 1->next canonicalisation
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=snowballstemmer&package-manager=pip&previous-version=3.0.1&new-version=3.1.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 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 669e0ce1dab..033a6a7badc 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -239,7 +239,7 @@ six==1.17.0 # via python-dateutil slotscheck==0.19.1 # via -r requirements/lint.in -snowballstemmer==3.0.1 +snowballstemmer==3.1.0 # via sphinx sphinx==8.1.3 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index 5b16877d993..280f50b2e74 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -232,7 +232,7 @@ six==1.17.0 # via python-dateutil slotscheck==0.19.1 # via -r requirements/lint.in -snowballstemmer==3.0.1 +snowballstemmer==3.1.0 # via sphinx sphinx==8.1.3 # via diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index 8c92b8157cb..38e8808825c 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -56,7 +56,7 @@ requests==2.34.2 # via # sphinx # sphinxcontrib-spelling -snowballstemmer==3.0.1 +snowballstemmer==3.1.0 # via sphinx sphinx==8.1.3 # via diff --git a/requirements/doc.txt b/requirements/doc.txt index 8d6f78a500c..ac53f3db591 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -52,7 +52,7 @@ pyyaml==6.0.3 # sphinxcontrib-mermaid requests==2.34.2 # via sphinx -snowballstemmer==3.0.1 +snowballstemmer==3.1.0 # via sphinx sphinx==8.1.3 # via From 400088ee767cd184e0257c0e9fd88940c0b6ea2d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 17:01:54 +0000 Subject: [PATCH 153/191] Bump pytest-codspeed from 5.0.2 to 5.0.3 (#12698) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pytest-codspeed](https://github.com/CodSpeedHQ/pytest-codspeed) from 5.0.2 to 5.0.3.
Release notes

Sourced from pytest-codspeed's releases.

v5.0.3

What's Changed

Full Changelog: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v5.0.2...v5.0.3

Changelog

Sourced from pytest-codspeed's changelog.

[5.0.3] - 2026-05-22

🏗️ Refactor

Commits
  • b2d12d8 Release v5.0.3 🚀
  • 31447b7 refactor: use instrument_hooks_callgrind_add_obj_skip from C API
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pytest-codspeed&package-manager=pip&previous-version=5.0.2&new-version=5.0.3)](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 +- 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 033a6a7badc..2c8aaf7e349 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -196,7 +196,7 @@ pytest==9.0.3 # pytest-mock # pytest-timeout # pytest-xdist -pytest-codspeed==5.0.2 +pytest-codspeed==5.0.3 # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 280f50b2e74..842ccb71917 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -191,7 +191,7 @@ pytest==9.0.3 # pytest-mock # pytest-timeout # pytest-xdist -pytest-codspeed==5.0.2 +pytest-codspeed==5.0.3 # via # -r requirements/lint.in # -r requirements/test-common.in diff --git a/requirements/lint.txt b/requirements/lint.txt index 1d05336f9f5..9d2c282b04b 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -87,7 +87,7 @@ pytest==9.0.3 # -r requirements/lint.in # pytest-codspeed # pytest-mock -pytest-codspeed==5.0.2 +pytest-codspeed==5.0.3 # via -r requirements/lint.in pytest-mock==3.15.1 # via -r requirements/lint.in diff --git a/requirements/test-common.txt b/requirements/test-common.txt index ee87da7fc03..40117fc2b88 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -74,7 +74,7 @@ pytest==9.0.3 # pytest-mock # pytest-timeout # pytest-xdist -pytest-codspeed==5.0.2 +pytest-codspeed==5.0.3 # via -r requirements/test-common.in pytest-cov==7.1.0 # via -r requirements/test-common.in diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 8385768b547..b73e0f51fe2 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -110,7 +110,7 @@ pytest==9.0.3 # pytest-mock # pytest-timeout # pytest-xdist -pytest-codspeed==5.0.2 +pytest-codspeed==5.0.3 # via -r requirements/test-common.in pytest-cov==7.1.0 # via -r requirements/test-common.in diff --git a/requirements/test.txt b/requirements/test.txt index 30587100a57..1b6dfdfb332 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -110,7 +110,7 @@ pytest==9.0.3 # pytest-mock # pytest-timeout # pytest-xdist -pytest-codspeed==5.0.2 +pytest-codspeed==5.0.3 # via -r requirements/test-common.in pytest-cov==7.1.0 # via -r requirements/test-common.in From b4153bab74481f431ed9305721f98f97447fc0dc Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 17:12:56 +0000 Subject: [PATCH 154/191] [PR #12677/7f0532c1 backport][3.14] Threat model chapter 3 (#12699) **This is a backport of PR #12677 as merged into master (7f0532c1df1915e4367ff4af25cd8453d2df9fc0).** Co-authored-by: Sam Bull --- THREAT_MODEL.md | 117 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index cb50d264489..3b6648685d5 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -429,3 +429,120 @@ Writer-level CR / LF / NUL rejection via `_safe_header` and `_write_str_raise_on_nlcr` has been in place since the header-injection family of issues was first surfaced (well before CVE-2023-37276, which was a parser-side fix). + +--- + +### 5.3. WebSocket framing & per-message deflate + +**Scope.** RFC 6455 frame parsing and serialisation, masking, fragmentation +and continuation, control frames (close / ping / pong), and the +permessage-deflate (PMCE; RFC 7692) extension. Both server-side +(`web_ws.WebSocketResponse`) and client-side (`client_ws.ClientWebSocketResponse`) +share this layer. Out of scope: the WebSocket *handshake* and per-side +lifecycle (covered in [§5.11](#511-server-side-websocket-handler) server, [§5.14](#514-client-side-websocket) client) and the underlying +compression codecs themselves ([§5.5](#55-compression-codecs)). + +**Components covered.** + +- `aiohttp/http_websocket.py` — public re-export shim. +- `aiohttp/_websocket/`: + - `mask.pyx` / `mask.pxd` — Cython SIMD-style XOR. + - `helpers.py` — pure-Python masking fallback (`bytearray.translate`), + extension parameter parsing (`ws_ext_parse`), close-code unpacking. + - `models.py` — `WSCloseCode`, `WSMsgType`, message dataclasses. + - `reader.py` / `reader_c.pyx` / `reader_c.py` / `reader_py.py` — frame + reader (Cython preferred, pure-Python fallback). + - `writer.py` — frame writer. + - `__init__.py`. + +**Selection.** The reader's import dance (`reader.py`) prefers +`reader_c` (Cython) and falls back to `reader_py` on `ImportError`. Both +implementations apply the same validation rules. + +**Trust boundaries & data flow.** + +```mermaid +flowchart LR + Wire([Untrusted peer]) --> Feed[reader.feed_data] + Feed --> Parse[Frame state machine] + Parse -->|opcode/RSV/length checks| Mask[websocket_mask XOR] + Mask --> Inflate[(zlib inflate if RSV1)] + Inflate --> Validate[max_msg_size + UTF-8] + Validate -->|WSMessage| App[(user code)] + App -->|send_*| Writer[writer.send_frame] + Writer --> Deflate[(zlib deflate if compress)] + Deflate --> Frame[Frame builder + mask] + Frame --> Wire +``` + +The reader's input is fully attacker-controlled. Output (`WSMessage` with +`data`, `extra`, `type`) is consumed by user handler code. Server-side, mask +bytes from the peer's frames are XORed before reaching the application; +client-side, the writer adds masks to outgoing frames. + +**Assets at risk (chunk-specific).** + +- **Frame integrity** — opcode, RSV bits, FIN, length, mask all parsed + consistently; no path can let a peer smuggle a control frame inside a data + frame or coerce the reader into accepting a malformed frame. +- **Decompression safety** — PMCE-compressed frames cannot drive memory or + CPU to denial of service via a small input expanding to a huge output. +- **Memory bounds across messages** — a peer holding the connection open and + drip-feeding fragments cannot grow memory unboundedly. + +**Threats (STRIDE).** + +| # | Component / Vector | STRIDE | Threat | Risk | +| :--- | :--- | :--- | :--- | :--- | +| 3.1 | Server reader accepting unmasked client frames | T / S | The reader does not enforce RFC 6455 §5.1 ("server MUST fail on unmasked client frame"). A non-conformant or malicious client can send unmasked frames; the spec rationale is preventing cache-poisoning of intermediaries. | Medium | +| 3.2 | Mask key generation | I | Outbound masks come from `random.getrandbits(32)` (`writer.py:WebSocketWriter.__init__`), not `secrets`. RFC 6455 §5.3 says masks must be "unpredictable"; PRNG is not cryptographic. | Low | +| 3.3 | Reserved bits (RSV1/2/3) | T | RSV2/3 always rejected; RSV1 only allowed when PMCE is negotiated (`reader_py.py:WebSocketReader._feed_data`). Misalignment between negotiation state and frame would let a peer toggle decompression. | Low | +| 3.4 | Unknown opcode | T | Peer-controlled opcode outside the defined range (`0x0`–`0xA`) could route to an unexpected handler path if accepted. | Low | +| 3.5 | Control-frame size > 125 bytes | T | Oversized control frame would violate RFC 6455 framing and could mis-frame against a non-aiohttp peer. | Low | +| 3.6 | Fragmented control frame (FIN=0) | T | Fragmented control frame is a protocol violation; accepting one would let a peer interleave control state across the fragment sequence. | Low | +| 3.7 | Continuation without preceding text/binary | T | Continuation frame without an initial data frame leaves assembly state ambiguous. | Low | +| 3.8 | Unbounded fragmentation memory growth | D | A peer streams many continuation fragments without ever setting FIN; the reassembly buffer grows with each fragment until memory is exhausted. | Low | +| 3.9 | PMCE decompression bomb | D | Compressed frame expanding to >`max_msg_size`. Mitigated by post-decompress check; some zlib backends (e.g. isal) may overshoot the per-call `max_length` by a chunk before the post-check rejects it. | Medium | +| 3.10 | PMCE context retention memory | D / I | When `server_no_context_takeover` / `client_no_context_takeover` is *not* negotiated, the zlib context persists across messages on each side. No explicit per-message reset; long-lived sessions accumulate state. | Low–Med | +| 3.11 | UTF-8 validation on text frames | T | Invalid UTF-8 in a text frame (or close reason) reaching the handler as `str` would surface as bytes via `surrogateescape` and confuse caller code that assumes valid Unicode. | Low | +| 3.12 | Close-frame handling | T | Out-of-range close codes from a peer would let a non-aiohttp consumer of the close reason mis-interpret the disconnect reason. | Low | +| 3.13 | Writer-side: large outbound message as single frame | D | Writer does not auto-fragment; a single `send_str(big_blob)` becomes one frame. Memory pressure on the local side and on intermediaries. | Low | +| 3.14 | Mask-on-send keys (Cython vs Python parity) | T | Divergence between `mask.pyx` and `helpers.py` `websocket_mask` would silently break receivers (one peer XORs with a different key than the other expects). | Low | +| 3.15 | Reader Cython vs pure-Python parity | T | Divergence between the two reader backends could let one silently accept a frame the other rejects, weakening protocol enforcement asymmetrically. | Low | + +**Mitigations.** + +| # | Threat | Existing | Recommended | +| :--- | :--- | :--- | :--- | +| 3.1 | Unmasked client frames accepted | None — the reader is direction-agnostic; `web_ws.py` does not enforce client-mask either. | **Recommended hardening: Enforce RFC 6455 §5.1 mask direction in strict mode only (gated on `DEBUG`, mirroring the HTTP parser's lenient-default / strict-DEBUG asymmetry): server reader rejects frames with `has_mask == 0`, client reader rejects masked server frames, both with a `PROTOCOL_ERROR`-style close. Production default stays lenient for interop.** | +| 3.2 | Non-cryptographic mask RNG | `partial(random.getrandbits, 32)` per writer instance. | Documented design decision: WebSocket masking exists for cache-poisoning resistance against intermediaries, not as a confidentiality primitive. The mask needs to be performant — called once per outbound frame on a hot path — and does not need to be cryptographically unpredictable. `random.getrandbits(32)` is the deliberate choice. | +| 3.3 | RSV bits | `reader_py.py:WebSocketReader._feed_data` ties RSV1 acceptance to the PMCE-negotiated `_compress` flag; RSV2/3 always rejected. | None. | +| 3.4 | Unknown opcode | Rejected. | None. | +| 3.5–3.7 | Control-frame and fragmentation rules | All enforced at reader. | None. | +| 3.8 | Fragment memory bound | `max_msg_size` enforced pre-FIN and at assembly. Default 4 MiB. | **User**: set a smaller `max_msg_size` for protocols where messages are bounded (e.g. chat); the 4 MiB default suits arbitrary payloads. | +| 3.9 | PMCE decompression bomb | `WebSocketReader._handle_frame` decompresses with a `max_length` of `max_msg_size + 1` and checks the result; on overflow, raises `MESSAGE_TOO_BIG` (1009). This `max_length` post-decompress check was introduced by PR #11898 (v3.13.3). | **Documented known limitation.** Some backends (notably `isal_zlib`) do not strictly honour `max_length` in `decompress()` and may overshoot by up to one zlib block before the post-decompress size check fires. The post-check still catches it before the bytes reach the application, but a transient over-allocation is possible. Document and monitor. | +| 3.10 | PMCE context retention | Default extensions request context takeover (per RFC 7692 default); user can negotiate `server_no_context_takeover` / `client_no_context_takeover` via handshake. | Documented design decision: keep the RFC 7692 default (context takeover). **Document the memory tradeoff in user-facing WebSocket docs.** **User**: configure no-context-takeover on long-lived sessions running on memory-constrained hosts. | +| 3.11 | UTF-8 validation | Strict `bytes.decode("utf-8")` post-assembly. | None. | +| 3.12 | Close-code validation | `reader_py.py:WebSocketReader._handle_frame` validates codes < 3000 against `ALLOWED_CLOSE_CODES`; codes ≥ 3000 accepted (RFC reserved for libraries / private use, correct). | None. | +| 3.13 | Writer single-frame size | None — caller-controlled. | **User**: chunk very large outbound payloads (beyond a few MiB) via fragmented messages; a single `send_*` becomes one frame and can pressure intermediaries. | +| 3.14 | Cython vs pure-Python mask parity | Both implement XOR on the same key cycling; behaviour identical. | Add a parameterised test that runs the mask helper against both backends side-by-side (see [§6.1](#61-highest-leverage-recommendations) #3). | +| 3.15 | Reader backend parity | `tests/test_websocket_parser.py` imports the single `WebSocketReader` symbol (whichever backend won the import), so each CI run only exercises one. | Parameterise like `tests/test_http_parser.py` does — explicitly import `WebSocketReaderPython` and `WebSocketReaderCython` (when available) and fixture-parametrise over both (see [§6.1](#61-highest-leverage-recommendations) #3). | + +**Past advisories / hardening (recap).** + +- **PR #11898** (3.13.3) — PMCE decompression DoS hardening: + `WebSocketReader._handle_frame` decompresses with a `max_length` cap of + `max_msg_size + 1` and rejects with `MESSAGE_TOO_BIG` (1009) on overflow. + This is the primary mitigation for zip-bomb-style attacks against + WebSocket peers. +- No formal CVE has been published against the WebSocket framing layer to + date. + +**Open questions.** + +1. Should server-side reader reject unmasked frames (and client-side reject + masked ones) per RFC 6455 §5.1? (Threat 3.1 — recommended.) +2. Is the PRNG mask source (`random.getrandbits`) sufficient, or should it be + migrated to `secrets`/`os.urandom`? (Threat 3.2.) +3. For long-lived WebSocket sessions, is there a use case for *forcing* + `no_context_takeover` defaults to limit memory growth? (Threat 3.10.) From afbc6bd3987121b142bf73b2331b022512b638f2 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 21:25:50 +0100 Subject: [PATCH 155/191] [PR #12673/93de128a backport][3.14] Reduce client timeout test runtime (#12705) **This is a backport of PR #12673 as merged into master (93de128a2ffa265e6f697dec11998745553977fa).** Co-authored-by: nightcityblade --- CHANGES/9705.contrib.rst | 2 ++ CONTRIBUTORS.txt | 1 + tests/test_client_functional.py | 8 ++++---- 3 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 CHANGES/9705.contrib.rst diff --git a/CHANGES/9705.contrib.rst b/CHANGES/9705.contrib.rst new file mode 100644 index 00000000000..5eaef0c4398 --- /dev/null +++ b/CHANGES/9705.contrib.rst @@ -0,0 +1,2 @@ +Reduced the runtime of a client timeout regression test by shortening its artificial response delay. +-- by :user:`nightcityblade`. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index ba7ce91cec1..01b6805cf6d 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -278,6 +278,7 @@ Moss Collum Mun Gwan-gyeong Navid Sheikhol Nicolas Braem +Night Cityblade Nikolay Kim Nikolay Novik Nikolay Tiunov diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 972de1e0e1c..ee07cbf1697 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -1158,17 +1158,17 @@ async def test_read_timeout_between_chunks(aiohttp_client, mocker) -> None: async def handler(request): resp = aiohttp.web.StreamResponse() await resp.prepare(request) - # write data 4 times, with pauses. Total time 2 seconds. + # write data 4 times, with pauses. Total time 0.4 seconds. for _ in range(4): - await asyncio.sleep(0.5) + await asyncio.sleep(0.1) await resp.write(b"data\n") return resp app = web.Application() app.add_routes([web.get("/", handler)]) - # A timeout of 0.2 seconds should apply per read. - timeout = aiohttp.ClientTimeout(sock_read=1) + # The read timeout should apply per read, not to the whole response. + timeout = aiohttp.ClientTimeout(sock_read=0.2) client = await aiohttp_client(app, timeout=timeout) res = b"" From 3a875d8e719a47c46443c5a93f99ebbc9ef8fb26 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 11:45:44 +0000 Subject: [PATCH 156/191] Bump python-discovery from 1.3.1 to 1.4.0 (#12712) 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.3.1 to 1.4.0.
Release notes

Sourced from python-discovery's releases.

v1.4.0

What's Changed

Full Changelog: https://github.com/tox-dev/python-discovery/compare/1.3.2...1.4.0

v1.3.2

What's Changed

Full Changelog: https://github.com/tox-dev/python-discovery/compare/1.3.1...1.3.2

Changelog

Sourced from python-discovery's changelog.

Features - 1.4.0

  • Add debug_build attribute to :class:PythonInfo exposing whether the interpreter is a debug build (Py_DEBUG) - by :user:gaborbernat. (:issue:80)

v1.3.2 (2026-05-27)


No significant changes.


v1.3.1 (2026-05-12)


Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=python-discovery&package-manager=pip&previous-version=1.3.1&new-version=1.4.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 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 2c8aaf7e349..99853e1a743 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -212,7 +212,7 @@ pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 # via freezegun -python-discovery==1.3.1 +python-discovery==1.4.0 # via virtualenv python-on-whales==0.81.0 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index 842ccb71917..f843a593cbc 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -207,7 +207,7 @@ pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 # via freezegun -python-discovery==1.3.1 +python-discovery==1.4.0 # via virtualenv python-on-whales==0.81.0 # via diff --git a/requirements/lint.txt b/requirements/lint.txt index 9d2c282b04b..2ad0cda2e37 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -93,7 +93,7 @@ pytest-mock==3.15.1 # via -r requirements/lint.in python-dateutil==2.9.0.post0 # via freezegun -python-discovery==1.3.1 +python-discovery==1.4.0 # via virtualenv python-on-whales==0.81.0 # via -r requirements/lint.in From a35b450adb41a7645908ec7c3911f7eeb07e104b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 12:00:55 +0000 Subject: [PATCH 157/191] Bump platformdirs from 4.9.6 to 4.10.0 (#12713) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [platformdirs](https://github.com/tox-dev/platformdirs) from 4.9.6 to 4.10.0.
Release notes

Sourced from platformdirs's releases.

4.10.0

What's Changed

New Contributors

Full Changelog: https://github.com/tox-dev/platformdirs/compare/4.9.6...4.10.0

Changelog

Sourced from platformdirs's changelog.

########### Changelog ###########


4.10.0 (2026-05-28)


  • ✨ feat: add user_publicshare_dir, user_templates_dir, user_fonts_dir, user_preference_dir :pr:491
  • ✨ feat: add user_projects_dir for $XDG_PROJECTS_DIR :pr:490
  • chore: improve platformdirs maintenance path :pr:488 - by :user:lphuc2250gma

4.9.6 (2026-04-09)


  • 🐛 fix(release): use double quotes for tag variable expansion :pr:477

4.9.5 (2026-04-06)


  • 📝 docs(appauthor): clarify None vs False on Windows :pr:476
  • Separates implementations of macOS dirs that share a default :pr:473 - by :user:Goddesen
  • Remove persist-credentials: false from release job :pr:472
  • fix: do not duplicate site dirs in Unix.iter_{config,site}_dirs() when use_site_for_root is active :pr:469 - by :user:viccie30
  • 🔧 fix(type): resolve ty 0.0.25 type errors :pr:468
  • 🔒 ci(workflows): add zizmor security auditing :pr:467
  • 🐛 fix(release): generate docstrfmt-compatible changelog entries :pr:463

4.9.4 (2026-03-05)


  • [pre-commit.ci] pre-commit autoupdate :pr:461 - by :user:pre-commit-ci[bot]
  • Update README.md
  • 📝 docs: add project logo to documentation :pr:459
  • Standardize .github files to .yaml suffix
  • build(deps): bump the all group with 2 updates :pr:457 - by :user:dependabot[bot]
  • Move SECURITY.md to .github/SECURITY.md
  • Add permissions to workflows :pr:455
  • Add security policy
  • [pre-commit.ci] pre-commit autoupdate :pr:454 - by :user:pre-commit-ci[bot]

4.9.2 (2026-02-16)


  • 📝 docs: restructure following Diataxis framework :pr:448

... (truncated)

Commits
  • 04cb136 Release 4.10.0
  • 078bc61 ✨ feat: add user_publicshare_dir, user_templates_dir, user_fonts_dir, user_pr...
  • d279747 ✨ feat: add user_projects_dir for $XDG_PROJECTS_DIR (#490)
  • 4116391 [pre-commit.ci] pre-commit autoupdate (#489)
  • dbc63f5 chore: improve platformdirs maintenance path (#488)
  • 9265108 [pre-commit.ci] pre-commit autoupdate (#487)
  • 9f857ec [pre-commit.ci] pre-commit autoupdate (#486)
  • a76e777 [pre-commit.ci] pre-commit autoupdate (#484)
  • 903fd9f [pre-commit.ci] pre-commit autoupdate (#483)
  • a5da35d build(deps): bump astral-sh/setup-uv from 8.0.0 to 8.1.0 in the all group (#482)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=platformdirs&package-manager=pip&previous-version=4.9.6&new-version=4.10.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 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 99853e1a743..1f81db53c19 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -152,7 +152,7 @@ pip-tools==7.5.3 # via -r requirements/dev.in pkgconfig==1.6.0 # via -r requirements/test-common.in -platformdirs==4.9.6 +platformdirs==4.10.0 # via # python-discovery # virtualenv diff --git a/requirements/dev.txt b/requirements/dev.txt index f843a593cbc..df3ac1d182a 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -149,7 +149,7 @@ pip-tools==7.5.3 # via -r requirements/dev.in pkgconfig==1.6.0 # via -r requirements/test-common.in -platformdirs==4.9.6 +platformdirs==4.10.0 # via # python-discovery # virtualenv diff --git a/requirements/lint.txt b/requirements/lint.txt index 2ad0cda2e37..cf361940101 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -62,7 +62,7 @@ packaging==26.2 # via pytest pathspec==1.1.1 # via mypy -platformdirs==4.9.6 +platformdirs==4.10.0 # via # python-discovery # virtualenv From 2d9fe794f3242a7bd4b9e130ff534d5c656a99f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 12:19:04 +0000 Subject: [PATCH 158/191] Bump virtualenv from 21.3.3 to 21.4.1 (#12716) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [virtualenv](https://github.com/pypa/virtualenv) from 21.3.3 to 21.4.1.
Release notes

Sourced from virtualenv's releases.

21.4.1

What's Changed

Full Changelog: https://github.com/pypa/virtualenv/compare/21.4.0...21.4.1

21.4.0

What's Changed

New Contributors

Full Changelog: https://github.com/pypa/virtualenv/compare/21.3.3...21.4.0

Changelog

Sourced from virtualenv's changelog.

Bugfixes - 21.4.1

  • Fix Windows debug build venvlauncher_d.exe substitution never triggering because executables() compared the source executable name instead of the target name, and fix AttributeError on debug_build attribute for interpreter info objects missing the field - by :user:gaborbernat. (:issue:3151)

v21.4.0 (2026-05-28)


Features - 21.4.0

  • Remove dead code targeting Python versions below the supported target range (PyPy 3.6, deprecated importlib APIs) and simplify the runtime import hook in _virtualenv.py - by :user:gaborbernat. (:issue:3149)
  • Support Windows debug builds (python_d.exe, venvlauncher_d.exe) matching CPython venv behavior, remove dead __SCRIPT_DIR__ replacement and has_shim version guard, drop unreachable Python 3.7 branch from pyvenv_launch_patch_active, and fix wheel deprecation message to say >= 3.9 - by :user:gaborbernat. (:issue:3150)

v21.3.3 (2026-05-13)


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 1f81db53c19..f7016461b45 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -307,7 +307,7 @@ uvloop==0.22.1 ; platform_system != "Windows" # -r requirements/lint.in valkey==6.1.1 # via -r requirements/lint.in -virtualenv==21.3.3 +virtualenv==21.4.1 # via pre-commit wait-for-it==2.3.0 # via -r requirements/test-common.in diff --git a/requirements/dev.txt b/requirements/dev.txt index df3ac1d182a..0a882012a66 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -297,7 +297,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.3.3 +virtualenv==21.4.1 # via pre-commit wait-for-it==2.3.0 # via -r requirements/test-common.in diff --git a/requirements/lint.txt b/requirements/lint.txt index cf361940101..a0a6a505ce4 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -128,7 +128,7 @@ uvloop==0.22.1 ; platform_system != "Windows" # via -r requirements/lint.in valkey==6.1.1 # via -r requirements/lint.in -virtualenv==21.3.3 +virtualenv==21.4.1 # via pre-commit zlib-ng==1.0.0 # via -r requirements/lint.in From 1e3ecd48ec423c1fcc729c98adff31172faae1ef Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 15:20:43 +0100 Subject: [PATCH 159/191] [PR #12689/07ed41bc backport][3.14] Reject all RFC 9110 forbidden control characters in outbound headers (#12717) **This is a backport of PR #12689 as merged into master (07ed41bc3bc0fe27316b168d2631fffaecba7d65).** Co-authored-by: Rodrigo Nogueira --- CHANGES/12689.breaking.rst | 7 ++++ THREAT_MODEL.md | 43 +++++++++++++++---------- aiohttp/_http_writer.pyx | 6 ++-- aiohttp/http_writer.py | 10 ++++-- tests/test_http_writer.py | 65 +++++++++++++++++++++++++++++++++----- 5 files changed, 103 insertions(+), 28 deletions(-) create mode 100644 CHANGES/12689.breaking.rst diff --git a/CHANGES/12689.breaking.rst b/CHANGES/12689.breaking.rst new file mode 100644 index 00000000000..9d807e00769 --- /dev/null +++ b/CHANGES/12689.breaking.rst @@ -0,0 +1,7 @@ +Tightened outbound header serialization to reject all ASCII control +characters forbidden by :rfc:`9110#section-5.5` and :rfc:`9112#section-4` +(``0x00``\-``0x08``, ``0x0A``\-``0x1F``, ``0x7F``) in status lines, +header field-names, and field-values. Previously only CR, LF and NUL were +rejected. HTAB (``0x09``) remains permitted in field values. Applications +that placed bare control characters in outbound headers will now raise +:exc:`ValueError` instead of emitting non-RFC-compliant bytes -- by :user:`rodrigobnogueira`. diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index 3b6648685d5..2834202427f 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -332,7 +332,8 @@ called out where the writer's safety depends on them). **Components covered.** - `aiohttp/_http_writer.pyx` — Cython `_serialize_headers` and - `_write_str_raise_on_nlcr` (the CR/LF/NUL bytewise rejector). + `_write_str_raise_on_nlcr` (the forbidden-CTL bytewise rejector: rejects + `0x00-0x08`, `0x0A-0x1F`, `0x7F`; HTAB and SP remain permitted). - `aiohttp/http_writer.py` — `StreamWriter` (the `AbstractStreamWriter` implementation) plus the pure-Python `_py_serialize_headers` / `_safe_header` fallback and the Cython/pure-Python switch at @@ -345,7 +346,9 @@ called out where the writer's safety depends on them). implementation; if `_http_writer` (Cython) imports successfully and `AIOHTTP_NO_EXTENSIONS` is unset, the Cython implementation replaces it (`http_writer.py:_py_serialize_headers`). Both implementations apply the same -CR / LF / NUL rejection on names *and* values *and* the status/request line. +RFC 9110 §5.5 / RFC 9112 §4 forbidden-CTL rejection (`0x00-0x08`, +`0x0A-0x1F`, `0x7F`; HTAB and SP permitted) on names *and* values *and* +the status/request line. **Trust boundaries & data flow.** @@ -353,7 +356,7 @@ CR / LF / NUL rejection on names *and* values *and* the status/request line. flowchart LR Handler([User handler / ClientRequest]) -->|status_line, headers, body| SW[StreamWriter] SW --> Serialize[_serialize_headers] - Serialize -->|reject CR/LF/NUL| Bytes[Wire bytes] + Serialize -->|reject forbidden CTLs| Bytes[Wire bytes] SW --> Body[write / write_eof / write_chunked] Body --> Bytes Bytes --> Transport[(asyncio Transport)] @@ -380,38 +383,38 @@ on the wire. The wire-side consumer is the **untrusted** counterparty | # | Component / Vector | STRIDE | Threat | Risk | | :--- | :--- | :--- | :--- | :--- | -| 2.1 | Header name/value with CR / LF / NUL | T / I | Response-splitting / header injection allowing the next "header" or even a complete second response/request to be appended on the wire. | High | +| 2.1 | Header name/value with forbidden CTL | T / I | Response-splitting / header injection (CR / LF) or non-RFC-compliant CTLs (`0x01-0x08`, `0x0B-0x1F`, `0x7F`) that downstream agents historically treat inconsistently. | High | | 2.2 | Status-line `reason` with CR / LF | T | Same family as 2.1 but on the status line; could let an attacker-controlled reason inject a body or a second status line. | High | -| 2.3 | Request-line path/method | T | Path-side smuggling via CR / LF / NUL or whitespace inside the path the writer emits. | Medium | +| 2.3 | Request-line path/method | T | Path-side smuggling via forbidden CTLs or whitespace inside the path the writer emits. | Medium | | 2.4 | `Content-Length` ≠ actual body length | T | If a handler / ClientRequest emits a body whose length disagrees with declared `Content-Length`, an intermediary may interpret framing differently from the writer's peer (smuggling). | Medium | | 2.5 | `Content-Length` *and* `Transfer-Encoding: chunked` | T | Both headers reach the wire if user code constructs them via the raw headers dict; intermediaries disagree on which wins. | Medium | | 2.6 | Body emission on HEAD / 1xx / 204 / 304 | T | Writer strips CL/TE for empty-body responses but **does not block the application from writing a body**; bytes after the `\r\n\r\n` confuse the next pipelined request. | Medium | -| 2.7 | `Set-Cookie` / `Cookie` value | T | Cookie name or value containing CR / LF / NUL passes through `SimpleCookie.output()` unchanged; only caught by writer's header validation. | Medium | +| 2.7 | `Set-Cookie` / `Cookie` value | T | Cookie name or value containing forbidden CTLs passes through `SimpleCookie.output()` unchanged; only caught by writer's header validation. | Medium | | 2.8 | Compression / `Content-Encoding` | T | Body double-compression when user sets `Content-Encoding` manually and also enables `compress=...`. Intermediaries may reject or mis-decode a doubly-compressed body. | Low | | 2.9 | Drain / backpressure on slow readers | D | Slow consumer (or `Sec-WebSocket-Key`-style hold) keeps `transport.write()` queued; writer drains at 64 KiB threshold (`http_writer.py:StreamWriter.write`). A handler that doesn't await `drain()` can blow up. | Medium | | 2.10 | Single oversized chunk | D | `write(b)` with a multi-GB blob is handed straight to `transport.write`; memory pressure shifts to asyncio's buffer. | Low | | 2.11 | Chunked encoding hex framing | T | Malformed chunk-size lines (negative values, leading-`+`, leading zeros, hex obfuscation) would let a non-aiohttp peer reframe the body differently and smuggle. | Low | -| 2.12 | Header insertion validation timing | T | CR/LF/NUL rejection is *write-time*, not *insert-time*. A handler that sets a malicious header and then aborts before `write_headers()` will not raise. (Documented; not a recommended change.) | Low | -| 2.13 | Cython ⇄ pure-Python parity | T | Divergence between the two `_serialize_headers` implementations could let one backend silently pass CR/LF/NUL that the other rejects, weakening egress safety asymmetrically. | Low | +| 2.12 | Header insertion validation timing | T | Forbidden-CTL rejection is *write-time*, not *insert-time*. A handler that sets a malicious header and then aborts before `write_headers()` will not raise. (Documented; not a recommended change.) | Low | +| 2.13 | Cython ⇄ pure-Python parity | T | Divergence between the two `_serialize_headers` implementations could let one backend silently pass a forbidden CTL that the other rejects, weakening egress safety asymmetrically. | Low | | 2.14 | Trailers asymmetry | T | The writer never emits trailers, but the parser accepts incoming trailers; not a writer-side threat in itself, just a documentation point for completeness. | Low | **Mitigations.** | # | Threat | Existing | Recommended | | :--- | :--- | :--- | :--- | -| 2.1 | Header CR / LF / NUL injection | Both backends reject these bytes via `_write_str_raise_on_nlcr` (`_http_writer.pyx:_write_str_raise_on_nlcr`) and `_safe_header` (`http_writer.py:_safe_header`), raising `ValueError` from `_serialize_headers` before any byte hits the transport. Applied symmetrically to names, values, and the status line. | **The current tests import whichever `_serialize_headers` won the import, so only one backend is exercised. Parameterise like `tests/test_http_parser.py` does (cross-cuts [§6.1](#61-highest-leverage-recommendations) #3).** | -| 2.2 | Status-line `reason` injection | `web_response.Response._set_status` (`web_response.py:StreamResponse._set_status`) rejects `\r` / `\n` in `reason` *at set-time*. The writer also rejects them at write-time as part of the status-line validation. | None. | -| 2.3 | Request-line path / method | The full status line (`{method} {path} HTTP/{v}.{v}`) goes through `_write_str_raise_on_nlcr` / `_safe_header`, so CR / LF / NUL are caught regardless of whether `path` came from `yarl` or `method` was a caller-supplied string. yarl additionally rejects these bytes earlier per RFC 3986. | None. | +| 2.1 | Header forbidden-CTL injection | Both backends reject the full RFC 9110 §5.5 / RFC 9112 §4 forbidden set (`0x00-0x08`, `0x0A-0x1F`, `0x7F`; HTAB and SP permitted) via `_write_str_raise_on_nlcr` (`_http_writer.pyx:_write_str_raise_on_nlcr`) and `_safe_header` (`http_writer.py:_safe_header`), raising `ValueError` from `_serialize_headers` before any byte hits the transport. Applied symmetrically to names, values, and the status line. | **The current tests import whichever `_serialize_headers` won the import, so only one backend is exercised. Parameterise like `tests/test_http_parser.py` does (cross-cuts [§6.1](#61-highest-leverage-recommendations) #3).** | +| 2.2 | Status-line `reason` injection | `web_response.Response._set_status` (`web_response.py:StreamResponse._set_status`) rejects `\r` / `\n` in `reason` *at set-time*. The writer also rejects the full forbidden-CTL set at write-time as part of the status-line validation. | None. | +| 2.3 | Request-line path / method | The full status line (`{method} {path} HTTP/{v}.{v}`) goes through `_write_str_raise_on_nlcr` / `_safe_header`, so forbidden CTLs are caught regardless of whether `path` came from `yarl` or `method` was a caller-supplied string. yarl additionally rejects CR/LF/NUL earlier per RFC 3986. | None. | | 2.4 | CL / body-length mismatch | None at write-time. `web.Response.write_eof` and the chunked writer write what they're given. | **Recommended hardening: in DEBUG mode, assert / warn when actual bytes-written disagrees with declared `Content-Length` at `write_eof()`. Useful for catching smuggling-adjacent bugs in user handlers.** | | 2.5 | CL + TE simultaneous | Server-side `enable_chunked_encoding()` (`web_response.py:StreamResponse.enable_chunked_encoding`) raises if `Content-Length` is already set; client-side `_update_transfer_encoding()` (`client_reqrep.py:ClientRequest._update_transfer_encoding`) raises if user sets `chunked=True` while `Content-Length` is in headers. Manual user injection into the raw headers dict is *not* caught. | **Consider a write-time assert in `StreamWriter` that rejects `Content-Length` and `Transfer-Encoding: chunked` coexisting.** | | 2.6 | Body-suppression edge cases | `web_response.py:StreamResponse._prepare_headers` strips `Content-Length` and `Transfer-Encoding` for HEAD / 1xx / 204 / 304 (`EMPTY_BODY_STATUS_CODES`, `helpers.py:EMPTY_BODY_METHODS`). The framework's own machinery doesn't write a body for these. | **User**: Do not call `resp.write(...)` in a handler responding HEAD / 1xx / 204 / 304 — framing strips CL / TE but does not block the byte write. **Optional aiohttp change: have `StreamWriter` short-circuit body writes when `length == 0` and the response was framed as empty-body.** | -| 2.7 | Cookie injection | `populate_with_cookies` (`helpers.py:populate_with_cookies`) routes the cookie through `SimpleCookie.output()` and then into a regular header, where the CR / LF / NUL check at write-time catches anything `SimpleCookie` happened to pass through. | Documented design decision: rely on writer-level validation rather than tightening `set_cookie` / `populate_with_cookies` further. Keep regression tests covering cookie name/value with CR / LF / NUL across both backends. | +| 2.7 | Cookie injection | `populate_with_cookies` (`helpers.py:populate_with_cookies`) routes the cookie through `SimpleCookie.output()` and then into a regular header, where the forbidden-CTL check at write-time catches anything `SimpleCookie` happened to pass through. | Documented design decision: rely on writer-level validation rather than tightening `set_cookie` / `populate_with_cookies` further. Keep regression tests covering cookie name/value with forbidden CTLs across both backends. | | 2.8 | Manual `Content-Encoding` | Server side: `enable_compression()` (`web_response.py:StreamResponse.enable_compression`) returns early if `Content-Encoding` already present, so the body is not double-compressed. Client side: `ClientRequest._update_content_encoding` raises `ValueError("compress can not be set if Content-Encoding header is set")` — symmetric guard. | None. | | 2.9 | Drain / backpressure | `StreamWriter.write` drains at `LIMIT = 0x10000` bytes (`http_writer.py:StreamWriter.write`) when `drain=True` is set by the caller. Application code is expected to `await write(...)` to honour backpressure. | **User**: `await write(...)` in handlers; tight `for` loops without `await` can starve the event loop. Cross-reference [§5.7](#57-server-connection-lifecycle) for connection-level read/write timeouts that mitigate slow consumers. | | 2.10 | Oversized single chunk | None at the writer layer — bytes go straight to `transport.write`. asyncio applies its own high-water marks via the transport. | **User**: Relies on application-level bounds (use streaming, generators, `FileResponse`, etc., for large bodies). | | 2.11 | Chunked hex framing | The writer always uses `f"{len(chunk):x}\r\n"` followed by the chunk and `\r\n` (`http_writer.py:StreamWriter._write_chunked_payload`). | None. | | 2.12 | Insert-time vs write-time validation | Headers are validated at write-time only; `set_status` validates `reason` at set-time. | Documented design decision: late validation is acceptable; keep behaviour as-is. | -| 2.13 | Cython ⇄ pure-Python parity | Both backends share the same logic and test surface; the Cython version uses a fast bytewise check, the Python version uses `in` on three sentinel characters. | **Parameterise the writer tests over both backends so egress equivalence on malicious inputs is exercised under both (see [§6.1](#61-highest-leverage-recommendations) #3).** | +| 2.13 | Cython ⇄ pure-Python parity | Both backends share the same logic and test surface; the Cython version uses a fast per-codepoint range check (`ch < 0x20 and ch != 0x09`, plus `0x7F`), the Python version uses a precompiled `re` over the same forbidden set. | **Parameterise the writer tests over both backends so egress equivalence on malicious inputs is exercised under both (see [§6.1](#61-highest-leverage-recommendations) #3).** | | 2.14 | Trailers asymmetry | Writer does not emit trailers; parser accepts trailers on incoming. Documented for completeness. | None. | **Past advisories / hardening (recap).** @@ -424,11 +427,19 @@ on the wire. The wire-side consumer is the **untrusted** counterparty the status-line `reason`. Fixed by rejecting CR/LF in `reason` at `_set_status` set-time, on top of the existing writer-side check (threat 2.2). - -Writer-level CR / LF / NUL rejection via `_safe_header` and +- **[#12689](https://github.com/aio-libs/aiohttp/pull/12689)** (hardening, no + CVE) — outbound header serialization only rejected CR/LF/NUL; other + RFC 9110 §5.5 / RFC 9112 §4 forbidden CTLs (`0x01-0x08`, `0x0B-0x1F`, + `0x7F`) could be emitted on the wire if a handler placed them into a + header. Tightened `_safe_header` and `_write_str_raise_on_nlcr` to + reject the full forbidden set (threat 2.1). + +Writer-level forbidden-CTL rejection via `_safe_header` and `_write_str_raise_on_nlcr` has been in place since the header-injection family of issues was first surfaced (well before CVE-2023-37276, which -was a parser-side fix). +was a parser-side fix); the rejected set was broadened from +{CR, LF, NUL} to the full RFC 9110 forbidden set in +[#12689](https://github.com/aio-libs/aiohttp/pull/12689). --- diff --git a/aiohttp/_http_writer.pyx b/aiohttp/_http_writer.pyx index ee8dcd7d2e9..7859ebd6682 100644 --- a/aiohttp/_http_writer.pyx +++ b/aiohttp/_http_writer.pyx @@ -111,9 +111,11 @@ cdef inline int _write_str_raise_on_nlcr(Writer* writer, object s): out_str = str(s) for ch in out_str: - if ch in {0x0D, 0x0A, 0x00}: + # https://www.rfc-editor.org/info/rfc9110/#section-5.5-5 + # https://www.rfc-editor.org/info/rfc9112/#section-4-3 + if (ch < 0x20 and ch != 0x09) or ch == 0x7F: raise ValueError( - "Newline, carriage return, or null byte detected in headers. " + "Forbidden control character detected in headers. " "Potential header injection attack." ) if _write_utf8(writer, ch) < 0: diff --git a/aiohttp/http_writer.py b/aiohttp/http_writer.py index 411a2aae882..a252d92effe 100644 --- a/aiohttp/http_writer.py +++ b/aiohttp/http_writer.py @@ -1,6 +1,7 @@ """Http related parsers and protocol.""" import asyncio +import re import sys from typing import ( # noqa TYPE_CHECKING, @@ -347,10 +348,15 @@ async def drain(self) -> None: await protocol._drain_helper() +# https://www.rfc-editor.org/info/rfc9110/#section-5.5-5 +# https://www.rfc-editor.org/info/rfc9112/#section-4-3 +_FORBIDDEN_HEADER_CHARS_RE = re.compile(r"[\x00-\x08\x0a-\x1f\x7f]") + + def _safe_header(string: str) -> str: - if "\r" in string or "\n" in string or "\x00" in string: + if _FORBIDDEN_HEADER_CHARS_RE.search(string) is not None: raise ValueError( - "Newline, carriage return, or null byte detected in headers. " + "Forbidden control character detected in headers. " "Potential header injection attack." ) return string diff --git a/tests/test_http_writer.py b/tests/test_http_writer.py index 3c667bb7344..0d052110242 100644 --- a/tests/test_http_writer.py +++ b/tests/test_http_writer.py @@ -984,10 +984,11 @@ async def test_write_headers_with_compression_coalescing( [ "\n", "\r", + "\x00", ], ) def test_serialize_headers_raises_on_new_line_or_carriage_return(char: str) -> None: - """Verify serialize_headers raises on cr or nl in the headers.""" + """Verify serialize_headers raises on cr, nl, or null byte in the headers.""" status_line = "HTTP/1.1 200 OK" headers = CIMultiDict( { @@ -1002,21 +1003,69 @@ def test_serialize_headers_raises_on_new_line_or_carriage_return(char: str) -> N _serialize_headers(status_line, headers) -def test_serialize_headers_raises_on_null_byte() -> None: +@pytest.mark.parametrize( + "char", + [chr(c) for c in (*range(0x01, 0x09), *range(0x0B, 0x20), 0x7F)], +) +def test_serialize_headers_raises_on_forbidden_control_chars_in_value( + char: str, +) -> None: + """Verify serialize_headers rejects RFC 9110-forbidden CTLs in values.""" status_line = "HTTP/1.1 200 OK" - headers = CIMultiDict( - { - hdrs.CONTENT_TYPE: "text/plain\x00", - } - ) + headers = CIMultiDict({hdrs.CONTENT_TYPE: f"text/plain{char}"}) with pytest.raises( ValueError, - match="null byte detected in headers", + match="Forbidden control character detected in headers", ): _serialize_headers(status_line, headers) +@pytest.mark.parametrize( + "char", + [chr(c) for c in (*range(0x01, 0x09), *range(0x0B, 0x20), 0x7F)], +) +def test_serialize_headers_raises_on_forbidden_control_chars_in_name( + char: str, +) -> None: + """Verify serialize_headers rejects RFC 9110-forbidden CTLs in names.""" + status_line = "HTTP/1.1 200 OK" + headers = CIMultiDict({f"X-Bad{char}Header": "value"}) + + with pytest.raises( + ValueError, + match="Forbidden control character detected in headers", + ): + _serialize_headers(status_line, headers) + + +@pytest.mark.parametrize( + "char", + [chr(c) for c in (*range(0x01, 0x09), *range(0x0B, 0x20), 0x7F)], +) +def test_serialize_headers_raises_on_forbidden_control_chars_in_status_line( + char: str, +) -> None: + """Verify serialize_headers rejects RFC 9110-forbidden CTLs in status line.""" + status_line = f"HTTP/1.1 200 OK{char}" + headers: CIMultiDict[str] = CIMultiDict() + + with pytest.raises( + ValueError, + match="Forbidden control character detected in headers", + ): + _serialize_headers(status_line, headers) + + +def test_serialize_headers_allows_htab_in_value() -> None: + """Verify HTAB (0x09) remains permitted in field values per RFC 9110.""" + status_line = "HTTP/1.1 200 OK" + headers = CIMultiDict({hdrs.CONTENT_TYPE: "text/plain\tcharset=utf-8"}) + + result = _serialize_headers(status_line, headers) + assert b"text/plain\tcharset=utf-8" in result + + async def test_write_compressed_data_with_headers_coalescing( buf: bytearray, protocol: BaseProtocol, From bf88077ebb14f4c29924b8e8904cba20c55c28b8 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 23:57:41 +0100 Subject: [PATCH 160/191] [PR #12719/879d48d1 backport][3.14] Reject invalid bytes in multipart/payload headers (#12720) **This is a backport of PR #12719 as merged into master (879d48d1619b9bc3662037afcb8bfa790a205e9b).** Co-authored-by: Sam Bull --- CHANGES/12706.bugfix.rst | 1 + aiohttp/payload.py | 8 +++++--- tests/test_payload.py | 15 +++++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 CHANGES/12706.bugfix.rst diff --git a/CHANGES/12706.bugfix.rst b/CHANGES/12706.bugfix.rst new file mode 100644 index 00000000000..9248585f99f --- /dev/null +++ b/CHANGES/12706.bugfix.rst @@ -0,0 +1 @@ +Fixed invalid bytes being allowed in multipart/payload headers -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/payload.py b/aiohttp/payload.py index dfc51831dad..b337bcf7795 100644 --- a/aiohttp/payload.py +++ b/aiohttp/payload.py @@ -23,6 +23,7 @@ parse_mimetype, sentinel, ) +from .http_writer import _safe_header from .streams import StreamReader from .typedefs import JSONBytesEncoder, JSONEncoder, _CIMultiDict @@ -197,9 +198,10 @@ def headers(self) -> _CIMultiDict: @property def _binary_headers(self) -> bytes: return ( - "".join([k + ": " + v + "\r\n" for k, v in self.headers.items()]).encode( - "utf-8" - ) + "".join( + _safe_header(k) + ": " + _safe_header(v) + "\r\n" + for k, v in self.headers.items() + ).encode("utf-8") + b"\r\n" ) diff --git a/tests/test_payload.py b/tests/test_payload.py index 3284c213d55..0fe57eeb679 100644 --- a/tests/test_payload.py +++ b/tests/test_payload.py @@ -101,6 +101,21 @@ def test_payload_content_type() -> None: assert p.content_type == "application/json" +@pytest.mark.parametrize("bad_byte", ("\r", "\n", "\x00")) +def test_binary_headers_reject_injection_in_value(bad_byte: str) -> None: + p = Payload("test", headers={"X-Custom": f"value{bad_byte}Injected: bad"}) + with pytest.raises(ValueError, match="header injection"): + p._binary_headers + + +@pytest.mark.parametrize("bad_byte", ("\r", "\n", "\x00")) +def test_binary_headers_reject_injection_in_name(bad_byte: str) -> None: + p = Payload("test") + p.headers[f"X-Custom{bad_byte}Injected"] = "value" + with pytest.raises(ValueError, match="header injection"): + p._binary_headers + + def test_bytes_payload_default_content_type() -> None: p = payload.BytesPayload(b"data") assert p.content_type == "application/octet-stream" From 221d02b882a162eee2b050692720aef6e1fa5865 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 02:04:34 +0100 Subject: [PATCH 161/191] [PR #12721/5a9777d0 backport][3.14] Reject invalid bytes in add_field() name/filename (#12722) **This is a backport of PR #12721 as merged into master (5a9777d09b460996f91f14520ce1a8056c06477c).** Co-authored-by: Sam Bull --- CHANGES/12722.bugfix.rst | 1 + aiohttp/formdata.py | 9 ++++----- tests/test_formdata.py | 16 +++++++++++++++- 3 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 CHANGES/12722.bugfix.rst diff --git a/CHANGES/12722.bugfix.rst b/CHANGES/12722.bugfix.rst new file mode 100644 index 00000000000..af8b103f566 --- /dev/null +++ b/CHANGES/12722.bugfix.rst @@ -0,0 +1 @@ +Fixed :py:meth:`~aiohttp.FormData.add_field` accepting invalid bytes in ``name`` and ``filename`` -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/formdata.py b/aiohttp/formdata.py index 5fe37f62dce..f602cdf2357 100644 --- a/aiohttp/formdata.py +++ b/aiohttp/formdata.py @@ -8,6 +8,7 @@ from . import hdrs, multipart, payload from .helpers import guess_filename +from .http_writer import _safe_header from .payload import Payload __all__ = ("FormData",) @@ -64,12 +65,14 @@ def add_field( warnings.warn(msg, DeprecationWarning) filename = name + _safe_header(name) type_options: MultiDict[str] = MultiDict({"name": name}) if filename is not None and not isinstance(filename, str): raise TypeError("filename must be an instance of str. Got: %s" % filename) if filename is None and isinstance(value, io.IOBase): filename = guess_filename(value, name) if filename is not None: + _safe_header(filename) type_options["filename"] = filename self._is_multipart = True @@ -79,11 +82,7 @@ def add_field( raise TypeError( "content_type must be an instance of str. Got: %s" % content_type ) - if "\r" in content_type or "\n" in content_type: - raise ValueError( - "Newline or carriage return detected in headers. " - "Potential header injection attack." - ) + _safe_header(content_type) headers[hdrs.CONTENT_TYPE] = content_type self._is_multipart = True if content_transfer_encoding is not None: diff --git a/tests/test_formdata.py b/tests/test_formdata.py index d826c92e67d..16839aa69a3 100644 --- a/tests/test_formdata.py +++ b/tests/test_formdata.py @@ -74,13 +74,27 @@ def test_invalid_type_formdata_content_type(val: object) -> None: form.add_field("foo", "bar", content_type=val) # type: ignore[arg-type] -@pytest.mark.parametrize("val", ("\r", "\n", "a\ra\n", "a\na\r")) +@pytest.mark.parametrize("val", ("\r", "\n", "a\ra\n", "a\na\r", "a\x00b")) def test_invalid_value_formdata_content_type(val: str) -> None: form = FormData() with pytest.raises(ValueError): form.add_field("foo", "bar", content_type=val) +@pytest.mark.parametrize("val", ("\r", "\n", "a\ra\n", "a\na\r", "a\x00b")) +def test_invalid_value_formdata_name(val: str) -> None: + form = FormData() + with pytest.raises(ValueError): + form.add_field(val, "bar") + + +@pytest.mark.parametrize("val", ("\r", "\n", "a\ra\n", "a\na\r", "a\x00b")) +def test_invalid_value_formdata_filename(val: str) -> None: + form = FormData() + with pytest.raises(ValueError): + form.add_field("foo", "bar", filename=val) + + def test_invalid_formdata_filename() -> None: form = FormData() invalid_vals = [0, 0.1, {}, [], b"foo"] From 9962023f625743c31004eb29b80eebb7819e207f Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Fri, 29 May 2026 03:21:05 +0100 Subject: [PATCH 162/191] Fix incorrect websocket upgrade (#12723) (#12724) (cherry picked from commit 2a4247b1adba3c9a6b1cb0bd6135f332f8ff2f3a) --- CHANGES/12727.bugfix.rst | 1 + aiohttp/test_utils.py | 5 ++++- aiohttp/web_ws.py | 2 +- tests/test_web_websocket_functional.py | 29 ++++++++++++++++++++++++-- 4 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 CHANGES/12727.bugfix.rst diff --git a/CHANGES/12727.bugfix.rst b/CHANGES/12727.bugfix.rst new file mode 100644 index 00000000000..d74b5eae2d3 --- /dev/null +++ b/CHANGES/12727.bugfix.rst @@ -0,0 +1 @@ +Fixed websocket upgrade occurring when header contained a value like `notupgrade` -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/test_utils.py b/aiohttp/test_utils.py index 92a49b975ec..6b50e23b810 100644 --- a/aiohttp/test_utils.py +++ b/aiohttp/test_utils.py @@ -733,6 +733,9 @@ def make_mocked_request( raw_hdrs = () chunked = "chunked" in headers.get(hdrs.TRANSFER_ENCODING, "").lower() + upgrade = headers.get(hdrs.CONNECTION, "").lower() == "upgrade" and bool( + headers.get(hdrs.UPGRADE) + ) message = RawRequestMessage( method, @@ -742,7 +745,7 @@ def make_mocked_request( raw_hdrs, closing, None, - False, + upgrade, chunked, URL(path), ) diff --git a/aiohttp/web_ws.py b/aiohttp/web_ws.py index c01d95637cd..f4cef84db53 100644 --- a/aiohttp/web_ws.py +++ b/aiohttp/web_ws.py @@ -287,7 +287,7 @@ def _handshake( ) ) - if "upgrade" not in headers.get(hdrs.CONNECTION, "").lower(): + if not request._message.upgrade: raise HTTPBadRequest( text=f"No CONNECTION upgrade hdr: {headers.get(hdrs.CONNECTION)}" ) diff --git a/tests/test_web_websocket_functional.py b/tests/test_web_websocket_functional.py index 1f34631279d..7fd084af588 100644 --- a/tests/test_web_websocket_functional.py +++ b/tests/test_web_websocket_functional.py @@ -32,8 +32,33 @@ async def handler(request): assert resp.status == 426 -async def test_websocket_json(loop, aiohttp_client) -> None: - async def handler(request): +async def test_handshake_connection_header_substring_not_a_token( + aiohttp_client: AiohttpClient, +) -> None: + async def handler(request: web.Request) -> web.WebSocketResponse: + ws = web.WebSocketResponse() + await ws.prepare(request) + await ws.close() + return ws + + app = web.Application() + app.router.add_route("GET", "/", handler) + client = await aiohttp_client(app) + + resp = await client.get( + "/", + headers={ + "Upgrade": "websocket", + "Connection": "keep-alive, notupgrade", + "Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==", + "Sec-WebSocket-Version": "13", + }, + ) + assert resp.status == 400 + + +async def test_websocket_json(aiohttp_client: AiohttpClient) -> None: + async def handler(request: web.Request) -> web.WebSocketResponse: ws = web.WebSocketResponse() if not ws.can_prepare(request): return web.HTTPUpgradeRequired() From 8bd9765fd7f0d9aac09ef6f30b908b276061e14c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 11:58:58 +0000 Subject: [PATCH 163/191] Bump slotscheck from 0.19.1 to 0.20.0 (#12731) Bumps [slotscheck](https://github.com/ariebovenberg/slotscheck) from 0.19.1 to 0.20.0.
Release notes

Sourced from slotscheck's releases.

0.20.0

  • Improve speed and accuracy of detecting of "slotless" classes, even if they are not pure Python (e.g. C extension modules).
  • Remove support for alternative Python implementations (e.g. PyPy) since they treat slots differently and running slotscheck wouldn't be useful. It also allows us to use CPython-specific features.
  • Improve accuracy of benchmarks on "errors and violations" page.
  • Add experimental --detect-unused-slots flag (Python 3.13+ only) to detect slots that are declared but never assigned within the class body. Disabled by default. Use --exclude-slots to suppress false positives.
Changelog

Sourced from slotscheck's changelog.

0.20.0 (2026-05-29)

  • Improve speed and accuracy of detecting of "slotless" classes, even if they are not pure Python (e.g. C extension modules).
  • Remove support for alternative Python implementations (e.g. PyPy) since they treat slots differently and running slotscheck wouldn't be useful. It also allows us to use CPython-specific features.
  • Improve accuracy of benchmarks on "errors and violations" page.
  • Add experimental --detect-unused-slots flag (Python 3.13+ only) to detect slots that are declared but never assigned within the class body. Disabled by default. Use --exclude-slots to suppress false positives. Abstract classes are automatically excluded from this check.
Commits
  • 26f749d Release 0.20 (#294)
  • 17d803e Merge pull request #293 from ariebovenberg/dependabot/pip/furo-gte-2025.12.19...
  • b9713b9 Update furo requirement from <2026,>=2024.8.6 to >=2025.12.19,<2026
  • 669a5eb Merge pull request #292 from ariebovenberg/dependabot/pip/tomli-2.4.1
  • 05909f4 Bump tomli from 2.4.0 to 2.4.1
  • 80b4169 Merge pull request #291 from ariebovenberg/dependabot/pip/tomli-2.4.0
  • a4b8340 Bump tomli from 2.3.0 to 2.4.0
  • c4f72c0 Merge pull request #290 from ariebovenberg/dependabot/pip/sphinx-click-approx...
  • 1c743d0 Update sphinx-click requirement from ~=6.0.0 to ~=6.2.0
  • 41f715c Merge pull request #289 from ariebovenberg/dependabot/pip/sphinx-gt-8-and-lt-10
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=slotscheck&package-manager=pip&previous-version=0.19.1&new-version=0.20.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 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 f7016461b45..d84fdbdcbfb 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -237,7 +237,7 @@ setuptools-git==1.2 # via -r requirements/test-common.in six==1.17.0 # via python-dateutil -slotscheck==0.19.1 +slotscheck==0.20.0 # via -r requirements/lint.in snowballstemmer==3.1.0 # via sphinx diff --git a/requirements/dev.txt b/requirements/dev.txt index 0a882012a66..8e75c8bb8a5 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -230,7 +230,7 @@ setuptools-git==1.2 # via -r requirements/test-common.in six==1.17.0 # via python-dateutil -slotscheck==0.19.1 +slotscheck==0.20.0 # via -r requirements/lint.in snowballstemmer==3.1.0 # via sphinx diff --git a/requirements/lint.txt b/requirements/lint.txt index a0a6a505ce4..055e397d7df 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -103,7 +103,7 @@ rich==15.0.0 # via pytest-codspeed six==1.17.0 # via python-dateutil -slotscheck==0.19.1 +slotscheck==0.20.0 # via -r requirements/lint.in tomli==2.4.1 # via From 193257dbb570f78072b67a32b0531fb6aae98577 Mon Sep 17 00:00:00 2001 From: timrid <6593626+timrid@users.noreply.github.com> Date: Fri, 29 May 2026 21:30:39 +0200 Subject: [PATCH 164/191] Add support for Android and iOS platforms (#12220) (#12733) --- .coveragerc-cython.toml | 3 + .coveragerc.toml | 3 + .github/workflows/ci-cd.yml | 68 +++++++++++++- CHANGES/11750.packaging.rst | 1 + pyproject.toml | 10 +- requirements/base-ft.txt | 6 +- requirements/base.txt | 6 +- requirements/constraints.txt | 26 +++--- requirements/dev.txt | 28 +++--- requirements/lint.txt | 2 +- requirements/runtime-deps.in | 6 +- requirements/runtime-deps.txt | 6 +- requirements/test-common-base.in | 9 ++ requirements/test-common-base.txt | 54 +++++++++++ requirements/test-common.in | 14 +-- requirements/test-common.txt | 22 ++--- requirements/test-ft.txt | 28 +++--- requirements/test-mobile.in | 8 ++ requirements/test-mobile.txt | 104 +++++++++++++++++++++ requirements/test.txt | 28 +++--- setup.cfg | 2 + tests/test_benchmarks_client.py | 8 +- tests/test_benchmarks_client_request.py | 11 ++- tests/test_benchmarks_client_ws.py | 8 +- tests/test_benchmarks_cookiejar.py | 9 +- tests/test_benchmarks_http_websocket.py | 8 +- tests/test_benchmarks_http_writer.py | 10 +- tests/test_benchmarks_web_fileresponse.py | 9 +- tests/test_benchmarks_web_middleware.py | 9 +- tests/test_benchmarks_web_request.py | 8 +- tests/test_benchmarks_web_response.py | 10 +- tests/test_benchmarks_web_urldispatcher.py | 9 +- tests/test_circular_imports.py | 3 + tests/test_client_functional.py | 3 +- tests/test_client_session.py | 7 +- tests/test_cookiejar.py | 3 + tests/test_http_parser.py | 3 + tests/test_leaks.py | 3 + tests/test_loop.py | 7 ++ tests/test_multipart.py | 2 +- tests/test_proxy_functional.py | 7 +- tests/test_run_app.py | 6 ++ tests/test_streams.py | 3 +- tests/test_urldispatch.py | 13 ++- tests/test_web_functional.py | 6 +- tests/test_web_response.py | 72 +++++++------- tests/test_web_sendfile_functional.py | 14 ++- 47 files changed, 507 insertions(+), 178 deletions(-) create mode 100644 CHANGES/11750.packaging.rst create mode 100644 requirements/test-common-base.in create mode 100644 requirements/test-common-base.txt create mode 100644 requirements/test-mobile.in create mode 100644 requirements/test-mobile.txt diff --git a/.coveragerc-cython.toml b/.coveragerc-cython.toml index 586dc937786..91336d47dd9 100644 --- a/.coveragerc-cython.toml +++ b/.coveragerc-cython.toml @@ -8,6 +8,9 @@ omit = [ ] [report] +partial_also = [ + 'if not TYPE_CHECKING', +] exclude_also = [ 'if TYPE_CHECKING', 'assert False', diff --git a/.coveragerc.toml b/.coveragerc.toml index 4ca5d2808bd..e08c0f24b8c 100644 --- a/.coveragerc.toml +++ b/.coveragerc.toml @@ -14,6 +14,9 @@ omit = [ ] [report] +partial_also = [ + 'if not TYPE_CHECKING', +] exclude_also = [ 'if TYPE_CHECKING', 'assert False', diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 67923da42c0..9bb01cb551c 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -255,6 +255,59 @@ jobs: report_type: test_results token: ${{ secrets.CODECOV_TOKEN }} + test-mobile: + permissions: + contents: read # to fetch code (actions/checkout) + + name: Test (${{ matrix.config.platform }}, ${{ matrix.pyver }}, ${{ matrix.config.os }}) + runs-on: ${{ matrix.config.os }} + needs: gen_llhttp + strategy: + matrix: + pyver: ["cp313", "cp314"] + config: + - os: ubuntu-latest + platform: android + archs: x86_64 + - os: macos-14 + platform: ios + archs: arm64_iphonesimulator + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: true + - name: Setup Python ${{ matrix.pyver }} + id: python-install + # important: do not use system python + env: + UV_PYTHON_PREFERENCE: only-managed + uses: astral-sh/setup-uv@v8.1.0 + with: + python-version: ${{ matrix.pyver }} + activate-environment: true + enable-cache: true + - name: Install build tooling and cython + run: | + uv pip install -U pip wheel setuptools build twine -r requirements/cython.in -c requirements/cython.txt + - name: Restore llhttp generated files + uses: actions/download-artifact@v8 + with: + name: llhttp + path: vendor/llhttp/build/ + - name: Cythonize + run: | + make cythonize + - name: Build wheels and test + uses: pypa/cibuildwheel@v3.4.1 + env: + CIBW_BUILD: ${{ matrix.pyver }}-* + CIBW_PLATFORM: ${{ matrix.config.platform }} + CIBW_ARCHS: ${{ matrix.config.archs }} + CIBW_TEST_REQUIRES: -r requirements/test-mobile.txt + CIBW_TEST_SOURCES: setup.cfg README.rst tests + CIBW_TEST_COMMAND: python -m pytest + autobahn: permissions: contents: read # to fetch code (actions/checkout) @@ -445,6 +498,7 @@ jobs: needs: - lint - test + - test-mobile - autobahn runs-on: ubuntu-latest @@ -506,7 +560,7 @@ jobs: path: dist build-wheels: - name: Build wheels on ${{ matrix.os }} ${{ matrix.qemu }} ${{ matrix.musl }} + name: Build wheels on ${{ matrix.os }} ${{ matrix.qemu }} ${{ matrix.musl }} ${{ matrix.platform }} runs-on: ${{ matrix.os }} needs: pre-deploy strategy: @@ -514,6 +568,7 @@ jobs: os: ["ubuntu-latest", "windows-latest", "windows-11-arm", "macos-latest", "ubuntu-24.04-arm"] qemu: [''] musl: [""] + platform: [""] include: # Split ubuntu/musl jobs for the sake of speed-up - os: ubuntu-latest @@ -549,6 +604,10 @@ jobs: musl: musllinux - os: ubuntu-24.04-arm musl: musllinux + - os: ubuntu-latest + platform: android + - os: macos-14 + platform: ios steps: - name: Checkout uses: actions/checkout@v6 @@ -603,14 +662,19 @@ jobs: # for those QEMU matrix cells. extras: uv env: + CIBW_PLATFORM: ${{ matrix.platform || 'auto' }} CIBW_SKIP: pp* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }} CIBW_ARCHS_MACOS: x86_64 arm64 universal2 + CIBW_ARCHS_IOS: arm64_iphoneos arm64_iphonesimulator x86_64_iphonesimulator + CIBW_ARCHS_ANDROID: arm64_v8a x86_64 - name: Upload wheels uses: actions/upload-artifact@v7 with: name: >- dist-${{ matrix.os }}-${{ matrix.musl }}-${{ - matrix.qemu + matrix.platform + && matrix.platform + || matrix.qemu && matrix.qemu || 'native' }} diff --git a/CHANGES/11750.packaging.rst b/CHANGES/11750.packaging.rst new file mode 100644 index 00000000000..2af5e11cf45 --- /dev/null +++ b/CHANGES/11750.packaging.rst @@ -0,0 +1 @@ +Added wheels for Android and iOS platforms -- by :user:`timrid`. diff --git a/pyproject.toml b/pyproject.toml index c74565e4902..bf490bf7cdc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,10 +49,10 @@ dynamic = [ [project.optional-dependencies] speedups = [ - "aiodns >= 3.3.0", - "Brotli >= 1.2; platform_python_implementation == 'CPython'", + "aiodns >= 3.3.0; sys_platform != 'android' and sys_platform != 'ios'", + "Brotli >= 1.2; platform_python_implementation == 'CPython' and sys_platform != 'android' and sys_platform != 'ios'", "brotlicffi >= 1.2; platform_python_implementation != 'CPython'", - "backports.zstd; platform_python_implementation == 'CPython' and python_version < '3.14'", + "backports.zstd; platform_python_implementation == 'CPython' and python_version < '3.14' and sys_platform != 'android' and sys_platform != 'ios'", ] [[project.maintainers]] @@ -171,6 +171,10 @@ test-command = "" # don't build PyPy wheels, install from source instead skip = "pp*" +[tool.cibuildwheel.ios] +# iOS currently does not support build[uv] +build-frontend = "build" + [tool.codespell] skip = '.git,*.pdf,*.svg,Makefile,CONTRIBUTORS.txt,venvs,_build' ignore-words-list = 'te,ue' diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt index 2592806157c..4a320fa2e85 100644 --- a/requirements/base-ft.txt +++ b/requirements/base-ft.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/base-ft.txt --strip-extras requirements/base-ft.in # -aiodns==4.0.4 +aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.2 # via -r requirements/runtime-deps.in @@ -14,9 +14,9 @@ async-timeout==5.0.1 ; python_version < "3.11" # via -r requirements/runtime-deps.in attrs==26.1.0 # via -r requirements/runtime-deps.in -backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" +backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" and sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in -brotli==1.2.0 ; platform_python_implementation == "CPython" +brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in cffi==2.0.0 # via pycares diff --git a/requirements/base.txt b/requirements/base.txt index e3c17b4f01d..5b50ef94765 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/base.txt --strip-extras requirements/base.in # -aiodns==4.0.4 +aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.2 # via -r requirements/runtime-deps.in @@ -14,9 +14,9 @@ async-timeout==5.0.1 ; python_version < "3.11" # via -r requirements/runtime-deps.in attrs==26.1.0 # via -r requirements/runtime-deps.in -backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" +backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" and sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in -brotli==1.2.0 ; platform_python_implementation == "CPython" +brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in cffi==2.0.0 # via pycares diff --git a/requirements/constraints.txt b/requirements/constraints.txt index d84fdbdcbfb..5745d103182 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/constraints.txt --strip-extras requirements/constraints.in # -aiodns==4.0.4 +aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via # -r requirements/lint.in # -r requirements/runtime-deps.in @@ -36,7 +36,7 @@ blockbuster==1.5.26 # via # -r requirements/lint.in # -r requirements/test-common.in -brotli==1.2.0 ; platform_python_implementation == "CPython" +brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in build==1.5.0 # via pip-tools @@ -83,7 +83,7 @@ forbiddenfruit==0.1.4 freezegun==1.5.5 # via # -r requirements/lint.in - # -r requirements/test-common.in + # -r requirements/test-common-base.in frozenlist==1.8.0 # via # -r requirements/runtime-deps.in @@ -151,7 +151,7 @@ pathspec==1.1.1 pip-tools==7.5.3 # via -r requirements/dev.in pkgconfig==1.6.0 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in platformdirs==4.10.0 # via # python-discovery @@ -167,7 +167,7 @@ propcache==0.5.2 # -r requirements/runtime-deps.in # yarl proxy-py==2.4.10 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in pycares==5.0.1 # via aiodns pycparser==3.0 @@ -190,7 +190,7 @@ pyproject-hooks==1.2.0 pytest==9.0.3 # via # -r requirements/lint.in - # -r requirements/test-common.in + # -r requirements/test-common-base.in # pytest-codspeed # pytest-cov # pytest-mock @@ -201,13 +201,13 @@ pytest-codspeed==5.0.3 # -r requirements/lint.in # -r requirements/test-common.in pytest-cov==7.1.0 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in pytest-mock==3.15.1 # via # -r requirements/lint.in - # -r requirements/test-common.in + # -r requirements/test-common-base.in pytest-timeout==2.4.0 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 @@ -223,10 +223,6 @@ pyyaml==6.0.3 # myst-parser # pre-commit # sphinxcontrib-mermaid -re-assert==1.1.0 - # via -r requirements/test-common.in -regex==2026.5.9 - # via re-assert requests==2.34.2 # via # sphinx @@ -234,7 +230,7 @@ requests==2.34.2 rich==15.0.0 # via pytest-codspeed setuptools-git==1.2 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in six==1.17.0 # via python-dateutil slotscheck==0.20.0 @@ -310,7 +306,7 @@ valkey==6.1.1 virtualenv==21.4.1 # via pre-commit wait-for-it==2.3.0 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in wheel==0.47.0 # via pip-tools yarl==1.24.2 diff --git a/requirements/dev.txt b/requirements/dev.txt index 8e75c8bb8a5..98ede6ba8d3 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/dev.txt --strip-extras requirements/dev.in # -aiodns==4.0.4 +aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via # -r requirements/lint.in # -r requirements/runtime-deps.in @@ -28,7 +28,7 @@ attrs==26.1.0 # via -r requirements/runtime-deps.in babel==2.18.0 # via sphinx -backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" +backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" and sys_platform != "android" and sys_platform != "ios" # via # -r requirements/lint.in # -r requirements/runtime-deps.in @@ -36,7 +36,7 @@ blockbuster==1.5.26 # via # -r requirements/lint.in # -r requirements/test-common.in -brotli==1.2.0 ; platform_python_implementation == "CPython" +brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in build==1.5.0 # via pip-tools @@ -81,7 +81,7 @@ forbiddenfruit==0.1.4 freezegun==1.5.5 # via # -r requirements/lint.in - # -r requirements/test-common.in + # -r requirements/test-common-base.in frozenlist==1.8.0 # via # -r requirements/runtime-deps.in @@ -148,7 +148,7 @@ pathspec==1.1.1 pip-tools==7.5.3 # via -r requirements/dev.in pkgconfig==1.6.0 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in platformdirs==4.10.0 # via # python-discovery @@ -164,7 +164,7 @@ propcache==0.5.2 # -r requirements/runtime-deps.in # yarl proxy-py==2.4.10 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in pycares==5.0.1 # via aiodns pycparser==3.0 @@ -185,7 +185,7 @@ pyproject-hooks==1.2.0 pytest==9.0.3 # via # -r requirements/lint.in - # -r requirements/test-common.in + # -r requirements/test-common-base.in # pytest-codspeed # pytest-cov # pytest-mock @@ -196,13 +196,13 @@ pytest-codspeed==5.0.3 # -r requirements/lint.in # -r requirements/test-common.in pytest-cov==7.1.0 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in pytest-mock==3.15.1 # via # -r requirements/lint.in - # -r requirements/test-common.in + # -r requirements/test-common-base.in pytest-timeout==2.4.0 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 @@ -218,16 +218,12 @@ pyyaml==6.0.3 # myst-parser # pre-commit # sphinxcontrib-mermaid -re-assert==1.1.0 - # via -r requirements/test-common.in -regex==2026.5.9 - # via re-assert requests==2.34.2 # via sphinx rich==15.0.0 # via pytest-codspeed setuptools-git==1.2 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in six==1.17.0 # via python-dateutil slotscheck==0.20.0 @@ -300,7 +296,7 @@ valkey==6.1.1 virtualenv==21.4.1 # via pre-commit wait-for-it==2.3.0 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in wheel==0.47.0 # via pip-tools yarl==1.24.2 diff --git a/requirements/lint.txt b/requirements/lint.txt index 055e397d7df..c294530dd86 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.10 # by the following command: # # pip-compile --allow-unsafe --output-file=requirements/lint.txt --resolver=backtracking --strip-extras requirements/lint.in diff --git a/requirements/runtime-deps.in b/requirements/runtime-deps.in index cf8f209b4ac..49ba703ef65 100644 --- a/requirements/runtime-deps.in +++ b/requirements/runtime-deps.in @@ -1,12 +1,12 @@ # Extracted from `pyproject.toml` via `make sync-direct-runtime-deps` -aiodns >= 3.3.0 +aiodns >= 3.3.0; sys_platform != 'android' and sys_platform != 'ios' aiohappyeyeballs >= 2.5.0 aiosignal >= 1.4.0 async-timeout >= 4.0, < 6.0 ; python_version < '3.11' attrs >= 17.3.0 -backports.zstd; platform_python_implementation == 'CPython' and python_version < '3.14' -Brotli >= 1.2; platform_python_implementation == 'CPython' +backports.zstd; platform_python_implementation == 'CPython' and python_version < '3.14' and sys_platform != 'android' and sys_platform != 'ios' +Brotli >= 1.2; platform_python_implementation == 'CPython' and sys_platform != 'android' and sys_platform != 'ios' brotlicffi >= 1.2; platform_python_implementation != 'CPython' frozenlist >= 1.1.1 multidict >=4.5, < 7.0 diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index d83995a61af..12fca0e33ef 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/runtime-deps.txt --strip-extras requirements/runtime-deps.in # -aiodns==4.0.4 +aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.2 # via -r requirements/runtime-deps.in @@ -14,9 +14,9 @@ async-timeout==5.0.1 ; python_version < "3.11" # via -r requirements/runtime-deps.in attrs==26.1.0 # via -r requirements/runtime-deps.in -backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" +backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" and sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in -brotli==1.2.0 ; platform_python_implementation == "CPython" +brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in cffi==2.0.0 # via pycares diff --git a/requirements/test-common-base.in b/requirements/test-common-base.in new file mode 100644 index 00000000000..7d7f58a13b0 --- /dev/null +++ b/requirements/test-common-base.in @@ -0,0 +1,9 @@ +freezegun +pkgconfig +proxy.py >= 2.4.4rc5 +pytest +pytest-cov +pytest-mock +pytest-timeout +setuptools-git +wait-for-it diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt new file mode 100644 index 00000000000..421a9cf1149 --- /dev/null +++ b/requirements/test-common-base.txt @@ -0,0 +1,54 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile --allow-unsafe --output-file=requirements/test-common-base.txt --strip-extras requirements/test-common-base.in +# +click==8.3.1 + # via wait-for-it +coverage==7.13.4 + # via pytest-cov +exceptiongroup==1.3.1 + # via pytest +freezegun==1.5.5 + # via -r requirements/test-common-base.in +iniconfig==2.3.0 + # via pytest +packaging==26.0 + # via pytest +pkgconfig==1.6.0 + # via -r requirements/test-common-base.in +pluggy==1.6.0 + # via + # pytest + # pytest-cov +proxy-py==2.4.10 + # via -r requirements/test-common-base.in +pygments==2.19.2 + # via pytest +pytest==9.0.2 + # via + # -r requirements/test-common-base.in + # pytest-cov + # pytest-mock + # pytest-timeout +pytest-cov==7.0.0 + # via -r requirements/test-common-base.in +pytest-mock==3.15.1 + # via -r requirements/test-common-base.in +pytest-timeout==2.4.0 + # via -r requirements/test-common-base.in +python-dateutil==2.9.0.post0 + # via freezegun +setuptools-git==1.2 + # via -r requirements/test-common-base.in +six==1.17.0 + # via python-dateutil +tomli==2.4.0 + # via + # coverage + # pytest +typing-extensions==4.15.0 + # via exceptiongroup +wait-for-it==2.3.0 + # via -r requirements/test-common-base.in diff --git a/requirements/test-common.in b/requirements/test-common.in index 77700881024..d3aa5d156b0 100644 --- a/requirements/test-common.in +++ b/requirements/test-common.in @@ -1,19 +1,11 @@ +-r test-common-base.in + blockbuster coverage -freezegun isal; python_version < "3.14" and implementation_name == "cpython" # no wheel for 3.14, no PyPy wheel for 1.8.0+ mypy; implementation_name == "cpython" -pkgconfig -proxy.py >= 2.4.4rc5 -pytest -pytest-cov -pytest-mock -pytest-timeout pytest-xdist pytest_codspeed python-on-whales -re-assert -setuptools-git -trustme; platform_machine != "i686" # no 32-bit wheels -wait-for-it +trustme; platform_machine != "i686" # no 32-bit wheels zlib_ng diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 40117fc2b88..6f9603b63b2 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -27,7 +27,7 @@ execnet==2.1.2 forbiddenfruit==0.1.4 # via blockbuster freezegun==1.5.5 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in idna==3.15 # via trustme iniconfig==2.3.0 @@ -49,13 +49,13 @@ packaging==26.2 pathspec==1.1.1 # via mypy pkgconfig==1.6.0 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in pluggy==1.6.0 # via # pytest # pytest-cov proxy-py==2.4.10 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in pycparser==3.0 # via cffi pydantic==2.13.4 @@ -68,7 +68,7 @@ pygments==2.20.0 # rich pytest==9.0.3 # via - # -r requirements/test-common.in + # -r requirements/test-common-base.in # pytest-codspeed # pytest-cov # pytest-mock @@ -77,25 +77,21 @@ pytest==9.0.3 pytest-codspeed==5.0.3 # via -r requirements/test-common.in pytest-cov==7.1.0 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in pytest-mock==3.15.1 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in pytest-timeout==2.4.0 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 # via freezegun python-on-whales==0.81.0 # via -r requirements/test-common.in -re-assert==1.1.0 - # via -r requirements/test-common.in -regex==2026.5.9 - # via re-assert rich==15.0.0 # via pytest-codspeed setuptools-git==1.2 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in six==1.17.0 # via python-dateutil tomli==2.4.1 @@ -117,6 +113,6 @@ typing-extensions==4.15.0 typing-inspection==0.4.2 # via pydantic wait-for-it==2.3.0 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in zlib-ng==1.0.0 # via -r requirements/test-common.in diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index b73e0f51fe2..e36986edaef 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/test-ft.txt --strip-extras requirements/test-ft.in # -aiodns==4.0.4 +aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.2 # via -r requirements/runtime-deps.in @@ -18,11 +18,11 @@ async-timeout==5.0.1 ; python_version < "3.11" # via -r requirements/runtime-deps.in attrs==26.1.0 # via -r requirements/runtime-deps.in -backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" +backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" and sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in blockbuster==1.5.26 # via -r requirements/test-common.in -brotli==1.2.0 ; platform_python_implementation == "CPython" +brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in cffi==2.0.0 # via @@ -43,7 +43,7 @@ execnet==2.1.2 forbiddenfruit==0.1.4 # via blockbuster freezegun==1.5.5 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in frozenlist==1.8.0 # via # -r requirements/runtime-deps.in @@ -79,7 +79,7 @@ packaging==26.2 pathspec==1.1.1 # via mypy pkgconfig==1.6.0 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in pluggy==1.6.0 # via # pytest @@ -89,7 +89,7 @@ propcache==0.5.2 # -r requirements/runtime-deps.in # yarl proxy-py==2.4.10 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in pycares==5.0.1 # via aiodns pycparser==3.0 @@ -104,7 +104,7 @@ pygments==2.20.0 # rich pytest==9.0.3 # via - # -r requirements/test-common.in + # -r requirements/test-common-base.in # pytest-codspeed # pytest-cov # pytest-mock @@ -113,25 +113,21 @@ pytest==9.0.3 pytest-codspeed==5.0.3 # via -r requirements/test-common.in pytest-cov==7.1.0 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in pytest-mock==3.15.1 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in pytest-timeout==2.4.0 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 # via freezegun python-on-whales==0.81.0 # via -r requirements/test-common.in -re-assert==1.1.0 - # via -r requirements/test-common.in -regex==2026.5.9 - # via re-assert rich==15.0.0 # via pytest-codspeed setuptools-git==1.2 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in six==1.17.0 # via python-dateutil tomli==2.4.1 @@ -156,7 +152,7 @@ typing-extensions==4.15.0 ; python_version < "3.13" typing-inspection==0.4.2 # via pydantic wait-for-it==2.3.0 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in yarl==1.24.2 # via -r requirements/runtime-deps.in zlib-ng==1.0.0 diff --git a/requirements/test-mobile.in b/requirements/test-mobile.in new file mode 100644 index 00000000000..72238fd941e --- /dev/null +++ b/requirements/test-mobile.in @@ -0,0 +1,8 @@ +-r base-ft.in +-r test-common-base.in + +# pip-compile does not support environment markers for transitive dependencies. So +# some packages that are transitive dependencies have to be explicitly excluded here: +backports-asyncio-runner; python_version < "3.11" # transitive dependency of "pytest-asyncio"; not installable on Python >= 3.11 using pip (uv works though) +cffi; sys_platform != 'android' and sys_platform != 'ios' # transitive dependency of "pycares" +pycares; sys_platform != 'android' and sys_platform != 'ios' # transitive dependency of "aiodns" diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt new file mode 100644 index 00000000000..f874c03ece9 --- /dev/null +++ b/requirements/test-mobile.txt @@ -0,0 +1,104 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile --allow-unsafe --output-file=requirements/test-mobile.txt --strip-extras requirements/test-mobile.in +# +aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" + # via -r requirements/runtime-deps.in +aiohappyeyeballs==2.6.2 + # via -r requirements/runtime-deps.in +aiosignal==1.4.0 + # via -r requirements/runtime-deps.in +async-timeout==5.0.1 ; python_version < "3.11" + # via -r requirements/runtime-deps.in +attrs==26.1.0 + # via -r requirements/runtime-deps.in +backports-asyncio-runner==1.2.0 ; python_version < "3.11" + # via -r requirements/test-mobile.in +backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" and sys_platform != "android" and sys_platform != "ios" + # via -r requirements/runtime-deps.in +brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios" + # via -r requirements/runtime-deps.in +cffi==2.0.0 ; sys_platform != "android" and sys_platform != "ios" + # via + # -r requirements/test-mobile.in + # pycares +click==8.4.0 + # via wait-for-it +coverage==7.14.0 + # via pytest-cov +exceptiongroup==1.3.1 + # via pytest +freezegun==1.5.5 + # via -r requirements/test-common-base.in +frozenlist==1.8.0 + # via + # -r requirements/runtime-deps.in + # aiosignal +gunicorn==26.0.0 + # via -r requirements/base-ft.in +idna==3.15 + # via yarl +iniconfig==2.3.0 + # via pytest +multidict==6.7.1 + # via + # -r requirements/runtime-deps.in + # yarl +packaging==26.2 + # via + # gunicorn + # pytest +pkgconfig==1.6.0 + # via -r requirements/test-common-base.in +pluggy==1.6.0 + # via + # pytest + # pytest-cov +propcache==0.5.2 + # via + # -r requirements/runtime-deps.in + # yarl +proxy-py==2.4.10 + # via -r requirements/test-common-base.in +pycares==5.0.1 ; sys_platform != "android" and sys_platform != "ios" + # via + # -r requirements/test-mobile.in + # aiodns +pycparser==3.0 + # via cffi +pygments==2.20.0 + # via pytest +pytest==9.0.3 + # via + # -r requirements/test-common-base.in + # pytest-cov + # pytest-mock + # pytest-timeout +pytest-cov==7.1.0 + # via -r requirements/test-common-base.in +pytest-mock==3.15.1 + # via -r requirements/test-common-base.in +pytest-timeout==2.4.0 + # via -r requirements/test-common-base.in +python-dateutil==2.9.0.post0 + # via freezegun +setuptools-git==1.2 + # via -r requirements/test-common-base.in +six==1.17.0 + # via python-dateutil +tomli==2.4.1 + # via + # coverage + # pytest +typing-extensions==4.15.0 ; python_version < "3.13" + # via + # -r requirements/runtime-deps.in + # aiosignal + # exceptiongroup + # multidict +wait-for-it==2.3.0 + # via -r requirements/test-common-base.in +yarl==1.24.2 + # via -r requirements/runtime-deps.in diff --git a/requirements/test.txt b/requirements/test.txt index 1b6dfdfb332..3cd82be4d49 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/test.txt --strip-extras requirements/test.in # -aiodns==4.0.4 +aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.2 # via -r requirements/runtime-deps.in @@ -18,11 +18,11 @@ async-timeout==5.0.1 ; python_version < "3.11" # via -r requirements/runtime-deps.in attrs==26.1.0 # via -r requirements/runtime-deps.in -backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" +backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" and sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in blockbuster==1.5.26 # via -r requirements/test-common.in -brotli==1.2.0 ; platform_python_implementation == "CPython" +brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in cffi==2.0.0 # via @@ -43,7 +43,7 @@ execnet==2.1.2 forbiddenfruit==0.1.4 # via blockbuster freezegun==1.5.5 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in frozenlist==1.8.0 # via # -r requirements/runtime-deps.in @@ -79,7 +79,7 @@ packaging==26.2 pathspec==1.1.1 # via mypy pkgconfig==1.6.0 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in pluggy==1.6.0 # via # pytest @@ -89,7 +89,7 @@ propcache==0.5.2 # -r requirements/runtime-deps.in # yarl proxy-py==2.4.10 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in pycares==5.0.1 # via aiodns pycparser==3.0 @@ -104,7 +104,7 @@ pygments==2.20.0 # rich pytest==9.0.3 # via - # -r requirements/test-common.in + # -r requirements/test-common-base.in # pytest-codspeed # pytest-cov # pytest-mock @@ -113,25 +113,21 @@ pytest==9.0.3 pytest-codspeed==5.0.3 # via -r requirements/test-common.in pytest-cov==7.1.0 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in pytest-mock==3.15.1 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in pytest-timeout==2.4.0 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 # via freezegun python-on-whales==0.81.0 # via -r requirements/test-common.in -re-assert==1.1.0 - # via -r requirements/test-common.in -regex==2026.5.9 - # via re-assert rich==15.0.0 # via pytest-codspeed setuptools-git==1.2 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in six==1.17.0 # via python-dateutil tomli==2.4.1 @@ -158,7 +154,7 @@ typing-inspection==0.4.2 uvloop==0.22.1 ; platform_system != "Windows" and implementation_name == "cpython" # via -r requirements/base.in wait-for-it==2.3.0 - # via -r requirements/test-common.in + # via -r requirements/test-common-base.in yarl==1.24.2 # via -r requirements/runtime-deps.in zlib-ng==1.0.0 diff --git a/setup.cfg b/setup.cfg index 6ea9b485db6..0f77c005a70 100644 --- a/setup.cfg +++ b/setup.cfg @@ -81,6 +81,8 @@ filterwarnings = # https://github.com/spulec/freezegun/issues/508 # https://github.com/spulec/freezegun/pull/511 ignore:datetime.*utcnow\(\) is deprecated and scheduled for removal:DeprecationWarning:freezegun.api + # coverage's C tracer is not available on iOS/Android (pure-Python fallback is used instead) + ignore:Couldn't import C tracer:coverage.exceptions.CoverageWarning # Weird issue in Python 3.13+ triggered in test_multipart.py ignore:coroutine method 'aclose' of 'BodyPartReader._decode_content_async' was never awaited:RuntimeWarning # Our own warning diff --git a/tests/test_benchmarks_client.py b/tests/test_benchmarks_client.py index 8ebedcc4776..d33936b5c8b 100644 --- a/tests/test_benchmarks_client.py +++ b/tests/test_benchmarks_client.py @@ -1,14 +1,20 @@ """codspeed benchmarks for HTTP client.""" import asyncio +from typing import TYPE_CHECKING import pytest -from pytest_codspeed import BenchmarkFixture from yarl import URL from aiohttp import hdrs, request, web from aiohttp.pytest_plugin import AiohttpClient, AiohttpServer +if TYPE_CHECKING: + from pytest_codspeed import BenchmarkFixture +else: + pytest_codspeed = pytest.importorskip("pytest_codspeed") + BenchmarkFixture = pytest_codspeed.BenchmarkFixture + def test_one_hundred_simple_get_requests( loop: asyncio.AbstractEventLoop, diff --git a/tests/test_benchmarks_client_request.py b/tests/test_benchmarks_client_request.py index 3a56e21d448..eeaf7329fea 100644 --- a/tests/test_benchmarks_client_request.py +++ b/tests/test_benchmarks_client_request.py @@ -2,9 +2,10 @@ import asyncio from http.cookies import BaseCookie +from typing import TYPE_CHECKING +import pytest from multidict import CIMultiDict -from pytest_codspeed import BenchmarkFixture from yarl import URL from aiohttp.client_reqrep import ClientRequest, ClientResponse @@ -13,6 +14,12 @@ from aiohttp.http_writer import HttpVersion11 from aiohttp.tracing import Trace +if TYPE_CHECKING: + from pytest_codspeed import BenchmarkFixture +else: + pytest_codspeed = pytest.importorskip("pytest_codspeed") + BenchmarkFixture = pytest_codspeed.BenchmarkFixture + def test_client_request_update_cookies( loop: asyncio.AbstractEventLoop, benchmark: BenchmarkFixture @@ -135,7 +142,6 @@ def write(self, data: bytes | bytearray | memoryview) -> None: """Swallow writes.""" class MockProtocol(asyncio.BaseProtocol): - def __init__(self) -> None: self.transport = MockTransport() self._paused = False @@ -151,7 +157,6 @@ def start_timeout(self) -> None: """Swallow start_timeout.""" class MockConnector: - def __init__(self) -> None: self.force_close = False diff --git a/tests/test_benchmarks_client_ws.py b/tests/test_benchmarks_client_ws.py index 0338b52fb9d..60b0a027133 100644 --- a/tests/test_benchmarks_client_ws.py +++ b/tests/test_benchmarks_client_ws.py @@ -1,14 +1,20 @@ """codspeed benchmarks for websocket client.""" import asyncio +from typing import TYPE_CHECKING import pytest -from pytest_codspeed import BenchmarkFixture from aiohttp import web from aiohttp._websocket.helpers import MSG_SIZE from aiohttp.pytest_plugin import AiohttpClient +if TYPE_CHECKING: + from pytest_codspeed import BenchmarkFixture +else: + pytest_codspeed = pytest.importorskip("pytest_codspeed") + BenchmarkFixture = pytest_codspeed.BenchmarkFixture + def test_one_thousand_round_trip_websocket_text_messages( loop: asyncio.AbstractEventLoop, diff --git a/tests/test_benchmarks_cookiejar.py b/tests/test_benchmarks_cookiejar.py index 78566151ef4..1f820cc061d 100644 --- a/tests/test_benchmarks_cookiejar.py +++ b/tests/test_benchmarks_cookiejar.py @@ -1,12 +1,19 @@ """codspeed benchmarks for cookies.""" from http.cookies import BaseCookie +from typing import TYPE_CHECKING -from pytest_codspeed import BenchmarkFixture +import pytest from yarl import URL from aiohttp.cookiejar import CookieJar +if TYPE_CHECKING: + from pytest_codspeed import BenchmarkFixture +else: + pytest_codspeed = pytest.importorskip("pytest_codspeed") + BenchmarkFixture = pytest_codspeed.BenchmarkFixture + async def test_load_cookies_into_temp_cookiejar(benchmark: BenchmarkFixture) -> None: """Benchmark for creating a temp CookieJar and filtering by URL. diff --git a/tests/test_benchmarks_http_websocket.py b/tests/test_benchmarks_http_websocket.py index 61b23125460..a5e506f8e9a 100644 --- a/tests/test_benchmarks_http_websocket.py +++ b/tests/test_benchmarks_http_websocket.py @@ -1,9 +1,9 @@ """codspeed benchmarks for http websocket.""" import asyncio +from typing import TYPE_CHECKING import pytest -from pytest_codspeed import BenchmarkFixture from aiohttp._websocket.helpers import MSG_SIZE, PACK_LEN3 from aiohttp._websocket.reader import WebSocketDataQueue @@ -11,6 +11,12 @@ from aiohttp.helpers import DEFAULT_CHUNK_SIZE from aiohttp.http_websocket import WebSocketReader, WebSocketWriter, WSMsgType +if TYPE_CHECKING: + from pytest_codspeed import BenchmarkFixture +else: + pytest_codspeed = pytest.importorskip("pytest_codspeed") + BenchmarkFixture = pytest_codspeed.BenchmarkFixture + def test_read_large_binary_websocket_messages( loop: asyncio.AbstractEventLoop, benchmark: BenchmarkFixture diff --git a/tests/test_benchmarks_http_writer.py b/tests/test_benchmarks_http_writer.py index 0d52ca875e6..d3cd1a72ea6 100644 --- a/tests/test_benchmarks_http_writer.py +++ b/tests/test_benchmarks_http_writer.py @@ -1,11 +1,19 @@ """codspeed benchmarks for http writer.""" +from typing import TYPE_CHECKING + +import pytest from multidict import CIMultiDict -from pytest_codspeed import BenchmarkFixture from aiohttp import hdrs from aiohttp.http_writer import _serialize_headers +if TYPE_CHECKING: + from pytest_codspeed import BenchmarkFixture +else: + pytest_codspeed = pytest.importorskip("pytest_codspeed") + BenchmarkFixture = pytest_codspeed.BenchmarkFixture + def test_serialize_headers(benchmark: BenchmarkFixture) -> None: """Benchmark 100 calls to _serialize_headers.""" diff --git a/tests/test_benchmarks_web_fileresponse.py b/tests/test_benchmarks_web_fileresponse.py index 01aa7448c86..ea7ef6c5ab0 100644 --- a/tests/test_benchmarks_web_fileresponse.py +++ b/tests/test_benchmarks_web_fileresponse.py @@ -2,13 +2,20 @@ import asyncio import pathlib +from typing import TYPE_CHECKING +import pytest from multidict import CIMultiDict -from pytest_codspeed import BenchmarkFixture from aiohttp import ClientResponse, web from aiohttp.pytest_plugin import AiohttpClient +if TYPE_CHECKING: + from pytest_codspeed import BenchmarkFixture +else: + pytest_codspeed = pytest.importorskip("pytest_codspeed") + BenchmarkFixture = pytest_codspeed.BenchmarkFixture + def test_simple_web_file_response( loop: asyncio.AbstractEventLoop, diff --git a/tests/test_benchmarks_web_middleware.py b/tests/test_benchmarks_web_middleware.py index 14aa269e360..b2d7b5035eb 100644 --- a/tests/test_benchmarks_web_middleware.py +++ b/tests/test_benchmarks_web_middleware.py @@ -1,13 +1,20 @@ """codspeed benchmarks for web middlewares.""" import asyncio +from typing import TYPE_CHECKING -from pytest_codspeed import BenchmarkFixture +import pytest from aiohttp import web from aiohttp.pytest_plugin import AiohttpClient from aiohttp.typedefs import Handler +if TYPE_CHECKING: + from pytest_codspeed import BenchmarkFixture +else: + pytest_codspeed = pytest.importorskip("pytest_codspeed") + BenchmarkFixture = pytest_codspeed.BenchmarkFixture + def test_ten_web_middlewares( benchmark: BenchmarkFixture, diff --git a/tests/test_benchmarks_web_request.py b/tests/test_benchmarks_web_request.py index 81afe7824e4..c27694d85b8 100644 --- a/tests/test_benchmarks_web_request.py +++ b/tests/test_benchmarks_web_request.py @@ -2,13 +2,19 @@ import asyncio import zlib +from typing import TYPE_CHECKING import pytest -from pytest_codspeed import BenchmarkFixture from aiohttp import web from aiohttp.pytest_plugin import AiohttpClient +if TYPE_CHECKING: + from pytest_codspeed import BenchmarkFixture +else: + pytest_codspeed = pytest.importorskip("pytest_codspeed") + BenchmarkFixture = pytest_codspeed.BenchmarkFixture + @pytest.mark.usefixtures("parametrize_zlib_backend") def test_read_compressed_post_body( diff --git a/tests/test_benchmarks_web_response.py b/tests/test_benchmarks_web_response.py index fbf1fadf1e1..a4246cea9ae 100644 --- a/tests/test_benchmarks_web_response.py +++ b/tests/test_benchmarks_web_response.py @@ -1,9 +1,17 @@ """codspeed benchmarks for the web responses.""" -from pytest_codspeed import BenchmarkFixture +from typing import TYPE_CHECKING + +import pytest from aiohttp import web +if TYPE_CHECKING: + from pytest_codspeed import BenchmarkFixture +else: + pytest_codspeed = pytest.importorskip("pytest_codspeed") + BenchmarkFixture = pytest_codspeed.BenchmarkFixture + def test_simple_web_response(benchmark: BenchmarkFixture) -> None: """Benchmark creating 100 simple web.Response.""" diff --git a/tests/test_benchmarks_web_urldispatcher.py b/tests/test_benchmarks_web_urldispatcher.py index f01adb6da5c..45f339b3274 100644 --- a/tests/test_benchmarks_web_urldispatcher.py +++ b/tests/test_benchmarks_web_urldispatcher.py @@ -6,18 +6,23 @@ import random import string from pathlib import Path -from typing import NoReturn, cast +from typing import TYPE_CHECKING, NoReturn, cast from unittest import mock import pytest from multidict import CIMultiDict, CIMultiDictProxy -from pytest_codspeed import BenchmarkFixture from yarl import URL import aiohttp from aiohttp import web from aiohttp.http import HttpVersion, RawRequestMessage +if TYPE_CHECKING: + from pytest_codspeed import BenchmarkFixture +else: + pytest_codspeed = pytest.importorskip("pytest_codspeed") + BenchmarkFixture = pytest_codspeed.BenchmarkFixture + @pytest.fixture def github_urls() -> list[str]: diff --git a/tests/test_circular_imports.py b/tests/test_circular_imports.py index 9b5d7ed2697..227da10e53b 100644 --- a/tests/test_circular_imports.py +++ b/tests/test_circular_imports.py @@ -85,6 +85,9 @@ def _discover_path_importables( ) +@pytest.mark.skipif( + sys.platform in ("android", "ios"), reason="subprocess not supported" +) @pytest.mark.parametrize( "import_path", _mark_aiohttp_worker_for_skipping(_find_all_importables(aiohttp)), diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index ee07cbf1697..aa3a6925c2f 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -1,4 +1,5 @@ # HTTP client functional tests against aiohttp.web server +from __future__ import annotations # TODO(PY311): Remove import asyncio import datetime @@ -25,7 +26,7 @@ except ImportError: import brotli except ImportError: - brotli = None # pragma: no cover + brotli = None try: from backports.zstd import ZstdCompressor diff --git a/tests/test_client_session.py b/tests/test_client_session.py index be9e77dfcc0..6b351687995 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -15,7 +15,6 @@ import pytest from multidict import CIMultiDict, MultiDict -from re_assert import Matches from yarl import URL import aiohttp @@ -435,10 +434,8 @@ async def make_sess(): return ClientSession(connector=connector, loop=loop) loop.run_until_complete(make_sess()) - assert ( - Matches("Session and connector has to use same event loop") - == str(ctx.value).strip() - ) + expected = "Session and connector has to use same event loop" + assert str(ctx.value).startswith(expected) another_loop.run_until_complete(connector.close()) diff --git a/tests/test_cookiejar.py b/tests/test_cookiejar.py index 6dac3a0adb4..fa1e31cd9f6 100644 --- a/tests/test_cookiejar.py +++ b/tests/test_cookiejar.py @@ -1678,6 +1678,9 @@ async def test_shared_cookie_with_multiple_domains() -> None: # === Security tests for restricted unpickler and JSON save/load === +@pytest.mark.skipif( + sys.platform in ("android", "ios"), reason="os.system not supported" +) async def test_load_rejects_malicious_pickle(tmp_path: Path) -> None: """Verify CookieJar.load() blocks arbitrary code execution via pickle. diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index 625c34533bc..d0fc90e6fc7 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -2605,6 +2605,9 @@ async def test_feed_eof_no_err_gzip(self, protocol: BaseProtocol) -> None: dbuf.feed_eof() assert buf._eof + @pytest.mark.skipif( + sys.platform in ("android", "ios"), reason="brotli not available" + ) async def test_feed_eof_no_err_brotli(self, protocol: BaseProtocol) -> None: buf = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) dbuf = DeflateBuffer(buf, "br") diff --git a/tests/test_leaks.py b/tests/test_leaks.py index 07b506bdb99..742d6f9aa65 100644 --- a/tests/test_leaks.py +++ b/tests/test_leaks.py @@ -8,6 +8,9 @@ IS_PYPY = platform.python_implementation() == "PyPy" +@pytest.mark.skipif( + sys.platform in ("android", "ios"), reason="subprocess not supported" +) @pytest.mark.skipif(IS_PYPY, reason="gc.DEBUG_LEAK not available on PyPy") @pytest.mark.parametrize( ("script", "message"), diff --git a/tests/test_loop.py b/tests/test_loop.py index a3520b457e4..aceec5a3998 100644 --- a/tests/test_loop.py +++ b/tests/test_loop.py @@ -1,5 +1,6 @@ import asyncio import platform +import sys import threading import pytest @@ -11,6 +12,9 @@ @pytest.mark.skipif( platform.system() == "Windows", reason="the test is not valid for Windows" ) +@pytest.mark.skipif( + sys.platform in ("android", "ios"), reason="subprocess not supported" +) async def test_subprocess_co(loop) -> None: proc = await asyncio.create_subprocess_shell( "exit 0", @@ -38,6 +42,9 @@ def test_default_loop(loop: asyncio.AbstractEventLoop) -> None: assert asyncio.get_event_loop() is loop +@pytest.mark.skipif( + sys.platform in ("android", "ios"), reason="subprocess not supported" +) def test_setup_loop_non_main_thread() -> None: child_exc = None diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 1f80f9c2a21..fb5bbe562d5 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -113,7 +113,7 @@ async def read(self, size=None): class TestMultipartResponseWrapper: - def test_at_eof(self) -> None: + async def test_at_eof(self) -> None: wrapper = MultipartResponseWrapper(mock.Mock(), mock.Mock()) wrapper.at_eof() assert wrapper.resp.content.at_eof.called diff --git a/tests/test_proxy_functional.py b/tests/test_proxy_functional.py index a240692d0d8..85309dc5cea 100644 --- a/tests/test_proxy_functional.py +++ b/tests/test_proxy_functional.py @@ -7,10 +7,10 @@ from collections.abc import Awaitable, Callable from contextlib import suppress from re import match as match_regex +from typing import TYPE_CHECKING from unittest import mock from uuid import uuid4 -import proxy import pytest from yarl import URL @@ -21,6 +21,11 @@ from aiohttp.pytest_plugin import AiohttpServer from aiohttp.test_utils import TestServer +if TYPE_CHECKING: + import proxy +else: + proxy = pytest.importorskip("proxy") + ASYNCIO_SUPPORTS_TLS_IN_TLS = sys.version_info >= (3, 11) pytestmark = [ diff --git a/tests/test_run_app.py b/tests/test_run_app.py index d9e21c696c0..e4acbb2cd21 100644 --- a/tests/test_run_app.py +++ b/tests/test_run_app.py @@ -686,6 +686,9 @@ def test_run_app_multiple_preexisting_sockets(patched_loop) -> None: """ +@pytest.mark.skipif( + sys.platform in ("android", "ios"), reason="subprocess not supported" +) def test_sigint() -> None: skip_if_on_windows() @@ -700,6 +703,9 @@ def test_sigint() -> None: assert proc.wait() == 0 +@pytest.mark.skipif( + sys.platform in ("android", "ios"), reason="subprocess not supported" +) def test_sigterm() -> None: skip_if_on_windows() diff --git a/tests/test_streams.py b/tests/test_streams.py index aa50b5ca2d3..ae3c731e477 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -9,7 +9,6 @@ from unittest import mock import pytest -from re_assert import Matches from aiohttp import streams from aiohttp.helpers import DEFAULT_CHUNK_SIZE @@ -1104,7 +1103,7 @@ async def test___repr__waiter(self) -> None: loop = asyncio.get_event_loop() stream = self._make_one() stream._waiter = loop.create_future() - assert Matches(r">") == repr(stream) + assert repr(stream).startswith(" None: handler2 = make_handler() router.add_route("GET", "/get", handler1, name="name") - regexp = "Duplicate 'name', already handled by" with pytest.raises(ValueError) as ctx: router.add_route("GET", "/get_other", handler2, name="name") - assert Matches(regexp) == str(ctx.value) + assert str(ctx.value).startswith("Duplicate 'name', already handled by") def test_route_plain(router) -> None: @@ -561,7 +559,7 @@ def test_contains(router) -> None: def test_static_repr(router) -> None: router.add_static("/get", pathlib.Path(aiohttp.__file__).parent, name="name") - assert Matches(r" None: @@ -688,7 +686,8 @@ async def test_regular_match_info(router) -> None: req = make_mocked_request("GET", "/get/john") match_info = await router.resolve(req) assert {"name": "john"} == match_info - assert Matches(">") == repr(match_info) + assert repr(match_info).startswith(" None: @@ -1021,7 +1020,7 @@ def test_static_route_user_home(router) -> None: except ValueError: # pragma: no cover pytest.skip("aiohttp folder is not placed in user's HOME") route = router.add_static("/st", str(static_dir)) - assert here == route.get_info()["directory"] + assert here.resolve() == route.get_info()["directory"] def test_static_route_points_to_file(router) -> None: @@ -1045,7 +1044,7 @@ async def test_405_for_resource_adapter(router) -> None: @pytest.mark.skipif(platform.system() == "Windows", reason="Different path formats") async def test_static_resource_outside_traversal(router: web.UrlDispatcher) -> None: """Test relative path traversing outside root does not resolve.""" - static_file = pathlib.Path(aiohttp.__file__) + static_file = pathlib.Path(aiohttp.__file__).resolve() request_path = "/st" + "/.." * (len(static_file.parts) - 2) + str(static_file) assert pathlib.Path(request_path).resolve() == static_file diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py index c50ceaad4c9..f054f71db21 100644 --- a/tests/test_web_functional.py +++ b/tests/test_web_functional.py @@ -31,7 +31,10 @@ try: import brotlicffi as brotli except ImportError: - import brotli + try: + import brotli + except ImportError: + brotli = None try: import ssl @@ -1199,6 +1202,7 @@ async def handler(request): await resp.release() +@pytest.mark.skipif(brotli is None, reason="brotli not available") async def test_response_with_precompressed_body_brotli(aiohttp_client) -> None: async def handler(request): headers = {"Content-Encoding": "br"} diff --git a/tests/test_web_response.py b/tests/test_web_response.py index 5299f1c8a3d..52a574f1c0c 100644 --- a/tests/test_web_response.py +++ b/tests/test_web_response.py @@ -11,7 +11,6 @@ import aiosignal import pytest from multidict import CIMultiDict, CIMultiDictProxy, MultiDict -from re_assert import Matches from aiohttp import HttpVersion, HttpVersion10, HttpVersion11, hdrs, web from aiohttp.abc import AbstractStreamWriter @@ -504,7 +503,7 @@ async def test_chunked_encoding_forbidden_for_http_10() -> None: with pytest.raises(RuntimeError) as ctx: await resp.prepare(req) - assert Matches("Using chunked encoding is forbidden for HTTP/1.0") == str(ctx.value) + assert str(ctx.value) == "Using chunked encoding is forbidden for HTTP/1.0" @pytest.mark.usefixtures("parametrize_zlib_backend") @@ -980,12 +979,12 @@ def test_response_cookies() -> None: resp.del_cookie("name") expected = ( - 'Set-Cookie: name=("")?; ' + 'Set-Cookie: name=""; ' "expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Path=/" ) - assert Matches(expected) == str(resp.cookies) + assert str(resp.cookies) == expected resp.del_cookie("name") - assert str(resp.cookies) == Matches(expected) + assert str(resp.cookies) == expected resp.set_cookie("name", "value", domain="local.host") expected = "Set-Cookie: name=value; Domain=local.host; Path=/" @@ -1047,10 +1046,10 @@ def test_response_cookie__issue_del_cookie() -> None: resp.del_cookie("name") expected = ( - 'Set-Cookie: name=("")?; ' + 'Set-Cookie: name=""; ' "expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Path=/" ) - assert Matches(expected) == str(resp.cookies) + assert str(resp.cookies) == expected def test_cookie_set_after_del() -> None: @@ -1368,15 +1367,14 @@ async def test_send_headers_for_empty_body(buf, writer) -> None: await resp.prepare(req) await resp.write_eof() txt = buf.decode("utf8") - assert ( - Matches( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 0\r\n" - "Date: .+\r\n" - "Server: .+\r\n\r\n" - ) - == txt - ) + + lines = txt.split("\r\n") + assert len(lines) == 6 + assert lines[0] == "HTTP/1.1 200 OK" + assert lines[1] == "Content-Length: 0" + assert lines[2].startswith("Date: ") + assert lines[3].startswith("Server: ") + assert lines[4] == lines[5] == "" async def test_render_with_body(buf, writer) -> None: @@ -1385,19 +1383,17 @@ async def test_render_with_body(buf, writer) -> None: await resp.prepare(req) await resp.write_eof() - txt = buf.decode("utf8") - assert ( - Matches( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 4\r\n" - "Content-Type: application/octet-stream\r\n" - "Date: .+\r\n" - "Server: .+\r\n\r\n" - "data" - ) - == txt - ) + + lines = txt.split("\r\n") + assert len(lines) == 7 + assert lines[0] == "HTTP/1.1 200 OK" + assert lines[1] == "Content-Length: 4" + assert lines[2] == "Content-Type: application/octet-stream" + assert lines[3].startswith("Date: ") + assert lines[4].startswith("Server: ") + assert lines[5] == "" + assert lines[6] == "data" async def test_multiline_reason(buf: bytearray, writer: AbstractStreamWriter) -> None: @@ -1412,18 +1408,16 @@ async def test_send_set_cookie_header(buf, writer) -> None: await resp.prepare(req) await resp.write_eof() - txt = buf.decode("utf8") - assert ( - Matches( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 0\r\n" - "Set-Cookie: name=value\r\n" - "Date: .+\r\n" - "Server: .+\r\n\r\n" - ) - == txt - ) + + lines = txt.split("\r\n") + assert len(lines) == 7 + assert lines[0] == "HTTP/1.1 200 OK" + assert lines[1] == "Content-Length: 0" + assert lines[2] == "Set-Cookie: name=value" + assert lines[3].startswith("Date: ") + assert lines[4].startswith("Server: ") + assert lines[5] == lines[6] == "" async def test_consecutive_write_eof() -> None: diff --git a/tests/test_web_sendfile_functional.py b/tests/test_web_sendfile_functional.py index b0a51b08dbb..5c6fd637944 100644 --- a/tests/test_web_sendfile_functional.py +++ b/tests/test_web_sendfile_functional.py @@ -19,7 +19,10 @@ try: import brotlicffi as brotli except ImportError: - import brotli + try: + import brotli + except ImportError: + brotli = None try: import ssl @@ -46,9 +49,12 @@ def hello_txt(request, tmp_path_factory) -> pathlib.Path: } # Uncompressed file is not actually written to test it is not required. hello["gzip"].write_bytes(gzip.compress(HELLO_AIOHTTP)) - hello["br"].write_bytes(brotli.compress(HELLO_AIOHTTP)) + if brotli is not None: + hello["br"].write_bytes(brotli.compress(HELLO_AIOHTTP)) hello["bzip2"].write_bytes(bz2.compress(HELLO_AIOHTTP)) encoding = getattr(request, "param", None) + if encoding == "br" and brotli is None: + pytest.skip("brotli not available") return hello[encoding] @@ -279,6 +285,8 @@ async def test_static_file_custom_content_type_compress( expect_encoding: str, ): """Test that custom type with encoding is returned for unencoded requests.""" + if expect_encoding == "br" and brotli is None: + pytest.skip("brotli not available") async def handler(request): resp = sender(hello_txt, chunk_size=16) @@ -314,6 +322,8 @@ async def test_static_file_with_encoding_and_enable_compression( forced_compression: web.ContentCoding | None, ): """Test that enable_compression does not double compress when an encoded file is also present.""" + if expect_encoding == "br" and brotli is None: + pytest.skip("brotli not available") async def handler(request): resp = sender(hello_txt) From a9ae4ae1637b61494f9222022c5ae5d7c9d834e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 19:55:51 +0000 Subject: [PATCH 165/191] Bump coverage from 7.13.4 to 7.14.1 (#12708) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.13.4 to 7.14.1.
Changelog

Sourced from coverage's changelog.

Version 7.14.1 — 2026-05-26

  • Fix: the HTML report used typographic niceties to make file paths more readable by adding a small amount of space around slashes. Those spaces interfered with searching the page for file paths of interest. Now the report uses CSS to accomplish the same visual tweak so that searches with slashes work correctly. Closes issue 2170_.

  • Add a 3.16 PyPI classifier <hugo-316_>_ since we test on the 3.16 main branch.

.. _issue 2170: coveragepy/coveragepy#2170 .. _hugo-316: https://mastodon.social/@​hugovk/116588523571204490

.. _changes_7-14-0:

Version 7.14.0 — 2026-05-10

  • Feature: now when running one of the reporting commands, if there are parallel data files that need combining, they will be implicitly combined before creating the report. There is no option to avoid the combination; let us know if you have a use case that requires it. Thanks, Tim Hatch <pull 2162_>. Closes issue 1781.

  • Fix: the output from combine was too verbose, listing each file considered. Now it shows a single line with the counts of files combined, files skipped, and files with errors. The -q flag suppresses this line. The old detailed lines are available with the new --debug=combine option.

  • Fix: running a Python file through a symlink now sets the sys.path correctly, matching regular Python behavior. Fixes issue 2157_.

  • Fix: Collector.flush_data could fail with "RuntimeError: Set changed size during iteration" when a tracer in another thread added a line to the per-file set that add_lines (or add_arcs) was iterating. The values passed to CoverageData are now snapshotted via dict.copy() and set.copy(), which are atomic under the GIL. Thanks, Alex Vandiver <pull 2165_>_.

  • Fix: the soft keyword lazy is now bolded in HTML reports.

  • We are no longer testing eventlet support. Eventlet started issuing stern deprecation warnings that break our tests. Our support code is still there.

.. _issue 1781: coveragepy/coveragepy#1781 .. _issue 2157: coveragepy/coveragepy#2157 .. _pull 2162: coveragepy/coveragepy#2162

... (truncated)

Commits
  • 64d9b66 docs: correct the date for 7.14.1
  • 6fa7dd4 chore: bump actions/dependency-review-action (#2181)
  • 078afae docs: sample HTML for 7.14.1
  • cb4f028 docs: prep for 7.14.1
  • ae2d09f Merge branch 'nedbat/classifire-316-kits'
  • 2c3568b build: declare 3.16 compatibility
  • faa68f8 chore: bump github/codeql-action in the action-dependencies group (#2173)
  • eb55fee test: we don't need PyPy < 7.3.22 anymore
  • ac168fe test: the text summary should show missing
  • fed4bd2 chore: upgrade virtualenv
  • Additional commits viewable in compare view

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 5745d103182..8a7f2f7e298 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -56,7 +56,7 @@ click==8.4.1 # slotscheck # towncrier # wait-for-it -coverage==7.14.0 +coverage==7.14.1 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/dev.txt b/requirements/dev.txt index 98ede6ba8d3..d7ae2cd8d5e 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -56,7 +56,7 @@ click==8.4.1 # slotscheck # towncrier # wait-for-it -coverage==7.14.0 +coverage==7.14.1 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt index 421a9cf1149..9065c1c2340 100644 --- a/requirements/test-common-base.txt +++ b/requirements/test-common-base.txt @@ -6,7 +6,7 @@ # click==8.3.1 # via wait-for-it -coverage==7.13.4 +coverage==7.14.1 # via pytest-cov exceptiongroup==1.3.1 # via pytest diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 6f9603b63b2..4d09da4cf73 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -14,7 +14,7 @@ cffi==2.0.0 # via cryptography click==8.4.1 # via wait-for-it -coverage==7.14.0 +coverage==7.14.1 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index e36986edaef..326a1acca56 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -30,7 +30,7 @@ cffi==2.0.0 # pycares click==8.4.1 # via wait-for-it -coverage==7.14.0 +coverage==7.14.1 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt index f874c03ece9..e12c7e3fab0 100644 --- a/requirements/test-mobile.txt +++ b/requirements/test-mobile.txt @@ -26,7 +26,7 @@ cffi==2.0.0 ; sys_platform != "android" and sys_platform != "ios" # pycares click==8.4.0 # via wait-for-it -coverage==7.14.0 +coverage==7.14.1 # via pytest-cov exceptiongroup==1.3.1 # via pytest diff --git a/requirements/test.txt b/requirements/test.txt index 3cd82be4d49..39a4b01e8d8 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -30,7 +30,7 @@ cffi==2.0.0 # pycares click==8.4.1 # via wait-for-it -coverage==7.14.0 +coverage==7.14.1 # via # -r requirements/test-common.in # pytest-cov From db3c18cc29f1b870367996c68ceced14f561cbac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 19:59:47 +0000 Subject: [PATCH 166/191] Bump idna from 3.15 to 3.17 (#12729) Bumps [idna](https://github.com/kjd/idna) from 3.15 to 3.17.
Changelog

Sourced from idna's changelog.

3.17 (2026-05-28)

  • Substantial 75% reduction in memory usage through new data structures and some optimization in processing speed.
  • Added a general 1024-character input length cap to the public validation, conversion, and codec entry points. This is well above any legitimate domain or label and guards against pathological inputs.

3.16 (2026-05-22)

  • Add a command-line interface (python -m idna, also available as the idna script). Encodes or decodes one or more domains supplied as arguments or on standard input, with options to select A-label or U-label output and control error handling.
  • Raise the minimum supported Python version to 3.9
  • Various code quality improvements
Commits
  • f48619c Release 3.17
  • 7421ba8 Pre-release 3.17rc0
  • 22ebb73 Merge pull request #251 from kjd/structure-optimizations
  • 2a7ac0a Drop redundant parallel-arrays comment from uts46data
  • 354eee9 Apply ruff format to uts46data.py
  • 8c34ffc Refactor uts46data into parallel arrays
  • 1189629 Range-encode joining_types for compact representation
  • f90b87a Generic length limit for functions
  • d6ffd28 Merge pull request #247 from kjd/release-3.16
  • 6d1a0de Release 3.16
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/base-ft.txt | 2 +- requirements/base.txt | 2 +- requirements/constraints.txt | 2 +- requirements/dev.txt | 2 +- requirements/doc-spelling.txt | 2 +- requirements/doc.txt | 2 +- requirements/lint.txt | 2 +- requirements/runtime-deps.txt | 2 +- requirements/test-common.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test-mobile.txt | 2 +- requirements/test.txt | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt index 4a320fa2e85..26f04fd4011 100644 --- a/requirements/base-ft.txt +++ b/requirements/base-ft.txt @@ -26,7 +26,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==26.0.0 # via -r requirements/base-ft.in -idna==3.15 +idna==3.17 # via yarl multidict==6.7.1 # via diff --git a/requirements/base.txt b/requirements/base.txt index 5b50ef94765..8713fc6262c 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -26,7 +26,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==26.0.0 # via -r requirements/base.in -idna==3.15 +idna==3.17 # via yarl multidict==6.7.1 # via diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 8a7f2f7e298..89b95c66ca4 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -92,7 +92,7 @@ gunicorn==26.0.0 # via -r requirements/base.in identify==2.6.19 # via pre-commit -idna==3.15 +idna==3.17 # via # requests # trustme diff --git a/requirements/dev.txt b/requirements/dev.txt index d7ae2cd8d5e..a1525e6247d 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -90,7 +90,7 @@ gunicorn==26.0.0 # via -r requirements/base.in identify==2.6.19 # via pre-commit -idna==3.15 +idna==3.17 # via # requests # trustme diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index 38e8808825c..2878edd94f6 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -20,7 +20,7 @@ docutils==0.21.2 # via # myst-parser # sphinx -idna==3.15 +idna==3.17 # via requests imagesize==2.0.0 # via sphinx diff --git a/requirements/doc.txt b/requirements/doc.txt index ac53f3db591..2503191af64 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -20,7 +20,7 @@ docutils==0.21.2 # via # myst-parser # sphinx -idna==3.15 +idna==3.17 # via requests imagesize==2.0.0 # via sphinx diff --git a/requirements/lint.txt b/requirements/lint.txt index c294530dd86..036cb2d6a8b 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -40,7 +40,7 @@ freezegun==1.5.5 # via -r requirements/lint.in identify==2.6.19 # via pre-commit -idna==3.15 +idna==3.17 # via trustme iniconfig==2.3.0 # via pytest diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index 12fca0e33ef..e86f12283b8 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -24,7 +24,7 @@ frozenlist==1.8.0 # via # -r requirements/runtime-deps.in # aiosignal -idna==3.15 +idna==3.17 # via yarl multidict==6.7.1 # via diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 4d09da4cf73..8d3bc4b479b 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -28,7 +28,7 @@ forbiddenfruit==0.1.4 # via blockbuster freezegun==1.5.5 # via -r requirements/test-common-base.in -idna==3.15 +idna==3.17 # via trustme iniconfig==2.3.0 # via pytest diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 326a1acca56..f2fd489e532 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -50,7 +50,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==26.0.0 # via -r requirements/base-ft.in -idna==3.15 +idna==3.17 # via # trustme # yarl diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt index e12c7e3fab0..9f18c58e8f6 100644 --- a/requirements/test-mobile.txt +++ b/requirements/test-mobile.txt @@ -38,7 +38,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==26.0.0 # via -r requirements/base-ft.in -idna==3.15 +idna==3.17 # via yarl iniconfig==2.3.0 # via pytest diff --git a/requirements/test.txt b/requirements/test.txt index 39a4b01e8d8..dcf60194f9f 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -50,7 +50,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==26.0.0 # via -r requirements/base.in -idna==3.15 +idna==3.17 # via # trustme # yarl From b00fa465c9018f9856b8a3051a96cc15612754ca Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 30 May 2026 15:04:15 +0100 Subject: [PATCH 167/191] [PR #12703/e221fdb2 backport][3.14] Fix irrelevant URLs to Gunicorn docs (#12735) **This is a backport of PR #12703 as merged into master (e221fdb2626336070c78d31932d55250bec523b4).** Co-authored-by: serhiiur --- CHANGES/12703.bugfix.rst | 1 + docs/deployment.rst | 10 +++++----- docs/logging.rst | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 CHANGES/12703.bugfix.rst diff --git a/CHANGES/12703.bugfix.rst b/CHANGES/12703.bugfix.rst new file mode 100644 index 00000000000..7cfca2415c3 --- /dev/null +++ b/CHANGES/12703.bugfix.rst @@ -0,0 +1 @@ +Fixed irrelevant URLs pointing to the corresponding sections in the documentation of Gunicorn -- by :user:`serhiiur`. diff --git a/docs/deployment.rst b/docs/deployment.rst index 60f218e848d..16c55b4567e 100644 --- a/docs/deployment.rst +++ b/docs/deployment.rst @@ -192,7 +192,7 @@ Nginx+Gunicorn ============== aiohttp can be deployed using `Gunicorn -`_, which is based on a +`_, which is based on a pre-fork worker model. Gunicorn launches your app as worker processes for handling incoming requests. @@ -264,10 +264,10 @@ Start Gunicorn -------------- When `Running Gunicorn -`_, you provide the name +`_, you provide the name of the module, i.e. *my_app_module*, and the name of the app or application factory, i.e. *my_web_app*, along with other `Gunicorn -Settings `_ provided +Settings `_ provided as command line flags or in your config file. In this case, we will use: @@ -279,7 +279,7 @@ In this case, we will use: * you may also want to use the ``--workers`` flag to tell Gunicorn how many worker processes to use for handling requests. (See the documentation for recommendations on `How Many Workers? - `_) + `_) * you may also want to use the ``--accesslog`` flag to enable the access log to be populated. (See :ref:`logging ` for more information.) @@ -390,7 +390,7 @@ More information ---------------- See the `official documentation -`_ for more +`_ for more information about suggested nginx configuration. You can also find out more about `configuring for secure https connections as well. `_ diff --git a/docs/logging.rst b/docs/logging.rst index 3f04c345112..b2a2a91dda4 100644 --- a/docs/logging.rst +++ b/docs/logging.rst @@ -160,7 +160,7 @@ instance) to the :meth:`aiohttp.web.AppRunner` constructor. .. _access_logformat: - http://docs.gunicorn.org/en/stable/settings.html#access-log-format + https://gunicorn.org/reference/settings/#access_log_format .. _accesslog: - http://docs.gunicorn.org/en/stable/settings.html#accesslog + https://gunicorn.org/reference/settings/#accesslog From fd7a4ac1afe3be3d89a3b5d8e0b95f5ce773952e Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 30 May 2026 18:46:23 +0100 Subject: [PATCH 168/191] [PR #12737/05129a00 backport][3.14] Fix pip-tools config (#12738) **This is a backport of PR #12737 as merged into master (05129a00e6037794d196ae11bd5f607770fe49bc).** Co-authored-by: Sam Bull --- .pip-tools.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.pip-tools.toml b/.pip-tools.toml index 82737a2d4f6..86969d6eda2 100644 --- a/.pip-tools.toml +++ b/.pip-tools.toml @@ -1,4 +1,5 @@ [pip-tools] -allow-unsafe = true +allow-unsafe = false resolver = "backtracking" strip-extras = true +unsafe-package = "aiohttp" From 2da99140b25de07d37ce75a4dcaf12b059502539 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 30 May 2026 21:00:40 +0100 Subject: [PATCH 169/191] [PR #12739/b3f2ff4e backport][3.14] Move pip-tools into pyproject.toml (#12740) **This is a backport of PR #12739 as merged into master (b3f2ff4e718a50a5636809c89059c045ce945a9b).** Co-authored-by: Sam Bull --- .pip-tools.toml | 5 ----- pyproject.toml | 7 +++++++ 2 files changed, 7 insertions(+), 5 deletions(-) delete mode 100644 .pip-tools.toml diff --git a/.pip-tools.toml b/.pip-tools.toml deleted file mode 100644 index 86969d6eda2..00000000000 --- a/.pip-tools.toml +++ /dev/null @@ -1,5 +0,0 @@ -[pip-tools] -allow-unsafe = false -resolver = "backtracking" -strip-extras = true -unsafe-package = "aiohttp" diff --git a/pyproject.toml b/pyproject.toml index bf490bf7cdc..4011ba49ce2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -175,6 +175,13 @@ skip = "pp*" # iOS currently does not support build[uv] build-frontend = "build" +[tool.pip-tools] +# Dependabot won't pick up .pip-tools.toml, so must be defined here. +allow-unsafe = false +resolver = "backtracking" +strip-extras = true +unsafe-package = ["aiohttp"] + [tool.codespell] skip = '.git,*.pdf,*.svg,Makefile,CONTRIBUTORS.txt,venvs,_build' ignore-words-list = 'te,ue' From 8e2f2325cf02812760214884a2bcdbe671c52da3 Mon Sep 17 00:00:00 2001 From: Puneet Dixit Date: Sun, 31 May 2026 20:37:17 +0530 Subject: [PATCH 170/191] [3.14] Improve HTTPS-on-HTTP parser error (#12743) --- CHANGES/10142.bugfix.rst | 1 + CONTRIBUTORS.txt | 1 + aiohttp/_http_parser.pyx | 2 ++ aiohttp/http_exceptions.py | 2 ++ tests/test_http_parser.py | 11 ++++++++++- 5 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 CHANGES/10142.bugfix.rst diff --git a/CHANGES/10142.bugfix.rst b/CHANGES/10142.bugfix.rst new file mode 100644 index 00000000000..821db59818d --- /dev/null +++ b/CHANGES/10142.bugfix.rst @@ -0,0 +1 @@ +Improved the parser error message shown when TLS handshake bytes are received on an HTTP port -- by :user:`puneetdixit200`. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 01b6805cf6d..1f94bac5c84 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -308,6 +308,7 @@ Phebe Polk Philipp A. Pierre-Louis Peeters Pieter van Beek +Puneet Dixit Qiao Han Rafael Viotti Rahul Nahata diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index 344be3123b0..8c2141fe51f 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -931,6 +931,8 @@ cdef parser_error_from_errno(cparser.llhttp_t* parser, data, pointer): cparser.HPE_INVALID_TRANSFER_ENCODING}: return BadHttpMessage(err_msg) elif errno == cparser.HPE_INVALID_METHOD: + if data.startswith(b"\x16\x03"): + return BadHttpMethod(error="Received HTTPS traffic on an HTTP port") return BadHttpMethod(error=err_msg) elif errno in {cparser.HPE_INVALID_STATUS, cparser.HPE_INVALID_VERSION, diff --git a/aiohttp/http_exceptions.py b/aiohttp/http_exceptions.py index f21a97f0170..54cc4af36be 100644 --- a/aiohttp/http_exceptions.py +++ b/aiohttp/http_exceptions.py @@ -109,6 +109,8 @@ class BadHttpMethod(BadStatusLine): """Invalid HTTP method in status line.""" def __init__(self, line: str = "", error: str | None = None) -> None: + if error is None and line.startswith("\x16\x03"): + error = "Received HTTPS traffic on an HTTP port" super().__init__(line, error or f"Bad HTTP method in status line {line!r}") diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index d0fc90e6fc7..011edbf912a 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -1517,7 +1517,16 @@ def test_http_request_parser_bad_method( ) -def test_http_request_parser_bad_version(parser) -> None: +def test_http_request_parser_tls_handshake_on_http_port( + parser: HttpRequestParser, +) -> None: + with pytest.raises(http_exceptions.BadHttpMethod) as ctx: + parser.feed_data(b"\x16\x03\x03\x01F\x01\r\n\r\n") + + assert "Received HTTPS traffic on an HTTP port" in str(ctx.value) + + +def test_http_request_parser_bad_version(parser: HttpRequestParser) -> None: with pytest.raises(http_exceptions.BadHttpMessage): parser.feed_data(b"GET //get HT/11\r\nHost: a\r\n\r\n") From 2049ba85701586fb065378787a74f99eb2b91108 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 31 May 2026 18:10:08 +0100 Subject: [PATCH 171/191] [PR #12674/e8832aee backport][3.14] Fix ZLibDecompressor dropping data past the first gzip member (#12745) **This is a backport of PR #12674 as merged into master (e8832aee7f694979292740b9b56a9c98fb0afc1a).** Co-authored-by: Ashutosh Kumar Singh <144926351+Ashutosh-177@users.noreply.github.com> --- CHANGES/7157.bugfix.rst | 6 +++ CONTRIBUTORS.txt | 1 + aiohttp/compression_utils.py | 36 +++++++++++++++- docs/spelling_wordlist.txt | 2 + tests/test_compression_utils.py | 36 ++++++++++++++++ tests/test_http_parser.py | 76 +++++++++++++++++++++++++++++++++ 6 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 CHANGES/7157.bugfix.rst diff --git a/CHANGES/7157.bugfix.rst b/CHANGES/7157.bugfix.rst new file mode 100644 index 00000000000..60f06d8da9f --- /dev/null +++ b/CHANGES/7157.bugfix.rst @@ -0,0 +1,6 @@ +Fixed ``ZLibDecompressor`` silently dropping data past the first +member when decompressing concatenated gzip/deflate streams. Each subsequent +member is now handed to a fresh decompressor, matching the behaviour already +implemented for ZSTD multi-frame streams. + +-- by :user:`Ashutosh-177` diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 1f94bac5c84..24139ce2cda 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -54,6 +54,7 @@ Arcadiy Ivanov Arseny Timoniq Artem Yushkovskiy Arthur Darcet +Ashutosh Kumar Singh Austin Scola Bai Haoran Ben Bader diff --git a/aiohttp/compression_utils.py b/aiohttp/compression_utils.py index 75b24d1cbbf..9313d21e009 100644 --- a/aiohttp/compression_utils.py +++ b/aiohttp/compression_utils.py @@ -55,6 +55,9 @@ def eof(self) -> bool: ... @property def unconsumed_tail(self) -> bytes: ... + @property + def unused_data(self) -> bytes: ... + class ZLibBackendProtocol(Protocol): MAX_WBITS: int @@ -275,15 +278,42 @@ def __init__( self._zlib_backend: Final = ZLibBackendWrapper(ZLibBackend._zlib_backend) self._decompressor = self._zlib_backend.decompressobj(wbits=self._mode) self._last_empty = False + self._pending_unused_data: bytes | None = None def decompress_sync( self, data: Buffer, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED ) -> bytes: + if self._pending_unused_data is not None: + data = self._pending_unused_data + bytes(data) + self._pending_unused_data = None result = self._decompressor.decompress( self._decompressor.unconsumed_tail + data, max_length ) # Only way to know that isal has no further data is checking we get no output self._last_empty = result == b"" + + # Handle concatenated gzip/deflate streams (multi-member). + # After a member ends, unused_data holds the start of the next member. + # Create a fresh decompressor for each subsequent member. + while self._decompressor.eof and self._decompressor.unused_data: + unused = self._decompressor.unused_data + self._decompressor = self._zlib_backend.decompressobj(wbits=self._mode) + if max_length != ZLIB_MAX_LENGTH_UNLIMITED: + max_length -= len(result) + if max_length <= 0: + self._pending_unused_data = unused + break + chunk = self._decompressor.decompress(unused, max_length) + self._last_empty = chunk == b"" + result += chunk + + # Member ended exactly at chunk boundary — no unused_data, but the + # next feed_data() call would fail on the spent decompressor. + # Only reset for gzip; deflate's feed_eof() relies on eof=True to + # confirm the stream is complete. + if self._decompressor.eof and self._mode > self._zlib_backend.MAX_WBITS: + self._decompressor = self._zlib_backend.decompressobj(wbits=self._mode) + return result def flush(self, length: int = 0) -> bytes: @@ -295,7 +325,11 @@ def flush(self, length: int = 0) -> bytes: @property def data_available(self) -> bool: - return bool(self._decompressor.unconsumed_tail) or not self._last_empty + return ( + bool(self._decompressor.unconsumed_tail) + or not self._last_empty + or self._pending_unused_data is not None + ) @property def eof(self) -> bool: diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index c10f10fdce4..1de034c393b 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -98,6 +98,7 @@ Cython Cythonize cythonized de +decompressor deduplicate defs Dependabot @@ -144,6 +145,7 @@ github google gunicorn gunicorn’s +gzip gzipped hackish highlevel diff --git a/tests/test_compression_utils.py b/tests/test_compression_utils.py index 3362b8feed0..5deebc8470d 100644 --- a/tests/test_compression_utils.py +++ b/tests/test_compression_utils.py @@ -1,5 +1,6 @@ """Tests for compression utils.""" +import gzip import sys import pytest @@ -87,3 +88,38 @@ def test_zstd_multi_frame_max_length_exhausted_preserves_unused_data() -> None: assert result1 == b"AAAA" result2 = d.decompress_sync(frame3) assert result2 == b"BBBBCCCC" + + +def test_zlib_gzip_multi_member_unlimited() -> None: + d = ZLibDecompressor(encoding="gzip") + member1 = gzip.compress(b"AAAA") + member2 = gzip.compress(b"BBBB") + result = d.decompress_sync(member1 + member2) + assert result == b"AAAABBBB" + + +def test_zlib_gzip_multi_member_max_length_partial() -> None: + d = ZLibDecompressor(encoding="gzip") + member1 = gzip.compress(b"AAAA") + member2 = gzip.compress(b"BBBB") + result = d.decompress_sync(member1 + member2, max_length=6) + assert result == b"AAAABB" + + +def test_zlib_gzip_multi_member_max_length_exhausted() -> None: + d = ZLibDecompressor(encoding="gzip") + member1 = gzip.compress(b"AAAA") + member2 = gzip.compress(b"BBBB") + result = d.decompress_sync(member1 + member2, max_length=4) + assert result == b"AAAA" + + +def test_zlib_gzip_multi_member_max_length_exhausted_preserves_unused_data() -> None: + d = ZLibDecompressor(encoding="gzip") + member1 = gzip.compress(b"AAAA") + member2 = gzip.compress(b"BBBB") + member3 = gzip.compress(b"CCCC") + result1 = d.decompress_sync(member1 + member2, max_length=4) + assert result1 == b"AAAA" + result2 = d.decompress_sync(member3) + assert result2 == b"BBBBCCCC" diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index 011edbf912a..5b5f3af6bc0 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -1,6 +1,7 @@ # Tests for aiohttp/protocol.py import asyncio +import gzip import platform import re import sys @@ -2550,6 +2551,81 @@ async def test_http_payload_zstandard_many_small_frames( assert b"".join(parts) == b"".join(out._buffer) assert out.is_eof() + async def test_http_payload_gzip_multi_member(self, protocol: BaseProtocol) -> None: + member1 = gzip.compress(b"first") + member2 = gzip.compress(b"second") + payload = member1 + member2 + out = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) + p = HttpPayloadParser( + out, + length=len(payload), + compression="gzip", + headers_parser=HeadersParser(), + ) + p.feed_data(payload) + assert b"firstsecond" == b"".join(out._buffer) + assert out.is_eof() + + async def test_http_payload_gzip_multi_member_chunked( + self, protocol: BaseProtocol + ) -> None: + member1 = gzip.compress(b"chunk1") + member2 = gzip.compress(b"chunk2") + out = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) + p = HttpPayloadParser( + out, + length=len(member1) + len(member2), + compression="gzip", + headers_parser=HeadersParser(), + ) + p.feed_data(member1) + p.feed_data(member2) + assert b"chunk1chunk2" == b"".join(out._buffer) + assert out.is_eof() + + async def test_http_payload_gzip_member_split_mid_chunk( + self, protocol: BaseProtocol + ) -> None: + member1 = gzip.compress(b"AAAA") + member2 = gzip.compress(b"BBBB") + combined = member1 + member2 + split_point = len(member1) + 3 # 3 bytes into member2 + out = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) + p = HttpPayloadParser( + out, + length=len(combined), + compression="gzip", + headers_parser=HeadersParser(), + ) + p.feed_data(combined[:split_point]) + p.feed_data(combined[split_point:]) + assert b"AAAABBBB" == b"".join(out._buffer) + assert out.is_eof() + + async def test_http_payload_gzip_many_small_members( + self, protocol: BaseProtocol + ) -> None: + parts = [f"part{i}".encode() for i in range(10)] + payload = b"".join(gzip.compress(p) for p in parts) + out = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) + p = HttpPayloadParser( + out, + length=len(payload), + compression="gzip", + headers_parser=HeadersParser(), + ) + p.feed_data(payload) + assert b"".join(parts) == b"".join(out._buffer) + assert out.is_eof() + class TestDeflateBuffer: async def test_feed_data(self, protocol: BaseProtocol) -> None: From 8206f2cf77bd253bf4375c1923e0d89546282526 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sun, 31 May 2026 21:20:16 +0100 Subject: [PATCH 172/191] Improve timing issues on shutdown tests (#12747) (#12748) (cherry picked from commit 1e14d35136fb17f1c92dd9ff2c555a5deff4e030) --- tests/test_run_app.py | 188 ++++++++++++++++++++++++++++++------------ 1 file changed, 133 insertions(+), 55 deletions(-) diff --git a/tests/test_run_app.py b/tests/test_run_app.py index e4acbb2cd21..19a5d021627 100644 --- a/tests/test_run_app.py +++ b/tests/test_run_app.py @@ -11,13 +11,20 @@ import time import traceback from collections.abc import AsyncIterator, Awaitable, Callable, Coroutine, Iterator -from typing import NoReturn +from typing import Any, NoReturn from unittest import mock from uuid import uuid4 import pytest -from aiohttp import ClientConnectorError, ClientSession, ClientTimeout, WSCloseCode, web +from aiohttp import ( + ClientConnectorError, + ClientSession, + ClientTimeout, + ServerDisconnectedError, + WSCloseCode, + web, +) from aiohttp.web_runner import BaseRunner # Test for features of OS' socket support @@ -1000,13 +1007,18 @@ async def stop(self, request: web.Request) -> web.Response: def run_app( self, sock: socket.socket, - timeout: int, + timeout: float, task: Callable[[], Coroutine[None, None, None]], extra_test: Callable[[ClientSession], Awaitable[None]] | None = None, ) -> tuple["asyncio.Task[None]", int]: num_connections = -1 t = test_task = None port = sock.getsockname()[1] + server_ready = asyncio.Event() + handler_started = asyncio.Event() + # Set from on_shutdown so per-test ``task()`` callbacks can complete + # deterministically during the shutdown_timeout window. + self._release_handler = asyncio.Event() class DictRecordClear(dict): def clear(self): @@ -1024,25 +1036,29 @@ def __init__(self, *args, **kwargs): self._connections = DictRecordClear() async def test() -> None: - await asyncio.sleep(0.5) + await server_ready.wait() async with ClientSession() as sess: - for _ in range(5): # pragma: no cover - try: - with pytest.raises(asyncio.TimeoutError): - async with sess.get( - f"http://127.0.0.1:{port}/", - timeout=ClientTimeout(total=0.1), - ): - pass - except ClientConnectorError: - await asyncio.sleep(0.5) - else: - break - async with sess.get(f"http://127.0.0.1:{port}/stop"): - pass + # Disable retries (same as TestClient). + sess._retry_connection = False + + async def long_poll() -> None: + async with sess.get(f"http://127.0.0.1:{port}/") as resp: + await resp.read() - if extra_test: - await extra_test(sess) + in_flight = asyncio.create_task(long_poll()) + try: + await asyncio.wait_for(handler_started.wait(), timeout=5) + # Handler is guaranteed in-flight here. Trigger shutdown. + async with sess.get(f"http://127.0.0.1:{port}/stop"): + pass + + if extra_test: + await extra_test(sess) + finally: + # If shutdown_timeout cancels the handler, the server + # tears down the transport mid-response. + with contextlib.suppress(ServerDisconnectedError): + await in_flight async def run_test(app: web.Application) -> None: nonlocal test_task @@ -1052,18 +1068,27 @@ async def run_test(app: web.Application) -> None: async def handler(request: web.Request) -> web.Response: nonlocal t + handler_started.set() t = asyncio.create_task(task()) await t return web.Response(text="FOO") - t = test_task = None + async def on_shutdown(app: web.Application) -> None: + self._release_handler.set() + + def on_ready(*args: object, **kwargs: object) -> None: + server_ready.set() + app = web.Application() app.cleanup_ctx.append(run_test) + app.on_shutdown.append(on_shutdown) app.router.add_get("/", handler) app.router.add_get("/stop", self.stop) with mock.patch("aiohttp.web_app.Server", ServerWithRecordClear): - web.run_app(app, sock=sock, shutdown_timeout=timeout) + # TODO: print is a bit of a hack, we should have a proper callback/condition + web.run_app(app, sock=sock, shutdown_timeout=timeout, print=on_ready) + assert test_task is not None assert test_task.exception() is None return t, num_connections @@ -1073,7 +1098,10 @@ def test_shutdown_wait_for_handler(self, unused_port_socket: socket.socket) -> N async def task(): nonlocal finished - await asyncio.sleep(2) + await self._release_handler.wait() + # Still doing work after the shutdown signal: the server must + # wait for this to complete rather than cancelling prematurely. + await asyncio.sleep(0.5) finished = True t, connection_count = self.run_app(sock, 3, task) @@ -1086,13 +1114,14 @@ async def task(): def test_shutdown_timeout_handler(self, unused_port_socket: socket.socket) -> None: sock = unused_port_socket finished = False + never = asyncio.Event() async def task(): nonlocal finished - await asyncio.sleep(2) - finished = True + await never.wait() + finished = True # pragma: no cover - t, connection_count = self.run_app(sock, 1, task) + t, connection_count = self.run_app(sock, 0.5, task) assert finished is False assert t.done() @@ -1104,21 +1133,21 @@ def test_shutdown_timeout_not_reached( ) -> None: sock = unused_port_socket finished = False + t_shutdown_began = 0.0 - async def task(): - nonlocal finished - await asyncio.sleep(1) + async def task() -> None: + nonlocal finished, t_shutdown_began + await self._release_handler.wait() + t_shutdown_began = time.monotonic() finished = True - start_time = time.time() - t, connection_count = self.run_app(sock, 15, task) assert finished is True assert t.done() assert connection_count == 0 # Verify run_app has not waited for timeout. - assert time.time() - start_time < 10 + assert time.monotonic() - t_shutdown_began < 10 def test_shutdown_new_conn_rejected( self, unused_port_socket: socket.socket @@ -1126,21 +1155,25 @@ def test_shutdown_new_conn_rejected( sock = unused_port_socket port = sock.getsockname()[1] finished = False + test_complete = asyncio.Event() async def task() -> None: nonlocal finished - await asyncio.sleep(9) + # Stay in-flight until the test confirms new-conn rejection. + await test_complete.wait() finished = True async def test(sess: ClientSession) -> None: - # Ensure we are in the middle of shutdown (waiting for task()). - await asyncio.sleep(1) + # release_handler is set from on_shutdown, so its set state + # means shutdown is now in progress (sites stopped). + await self._release_handler.wait() with pytest.raises(ClientConnectorError): # Use a new session to try and open a new connection. async with ClientSession() as sess: async with sess.get(f"http://127.0.0.1:{port}/"): pass assert finished is False + test_complete.set() t, connection_count = self.run_app(sock, 10, task, test) @@ -1154,22 +1187,28 @@ def test_shutdown_pending_handler_responds( sock = unused_port_socket port = sock.getsockname()[1] finished = False + t = None + server_ready = asyncio.Event() + handler_started = asyncio.Event() + release_handler = asyncio.Event() async def test() -> None: + await server_ready.wait() + async def test_resp(sess: ClientSession) -> None: async with sess.get(f"http://127.0.0.1:{port}/") as resp: assert await resp.text() == "FOO" - await asyncio.sleep(1) async with ClientSession() as sess: t = asyncio.create_task(test_resp(sess)) - await asyncio.sleep(1) + await asyncio.wait_for(handler_started.wait(), timeout=5) # Handler is in-progress while we trigger server shutdown. async with sess.get(f"http://127.0.0.1:{port}/stop"): pass assert finished is False - # Handler should still complete and produce a response. + # Handler should still complete and produce a response; + # release_handler is set from on_shutdown. await t async def run_test(app: web.Application) -> None: @@ -1180,17 +1219,27 @@ async def run_test(app: web.Application) -> None: async def handler(request: web.Request) -> web.Response: nonlocal finished - await asyncio.sleep(3) + handler_started.set() + await release_handler.wait() + # Still doing work after the shutdown signal: the server must + # wait for this to complete and deliver the response. + await asyncio.sleep(0.5) finished = True return web.Response(text="FOO") - t = None + async def on_shutdown(app: web.Application) -> None: + release_handler.set() + + def on_ready(*args: Any, **kwargs: Any) -> None: + server_ready.set() + app = web.Application() app.cleanup_ctx.append(run_test) + app.on_shutdown.append(on_shutdown) app.router.add_get("/", handler) app.router.add_get("/stop", self.stop) - web.run_app(app, sock=sock, shutdown_timeout=5) + web.run_app(app, sock=sock, shutdown_timeout=5, print=on_ready) assert t is not None assert t.exception() is None assert finished is True @@ -1201,15 +1250,16 @@ def test_shutdown_close_idle_keepalive( sock = unused_port_socket port = sock.getsockname()[1] t = None + server_ready = asyncio.Event() async def test() -> None: - await asyncio.sleep(1) + await server_ready.wait() async with ClientSession() as sess: async with sess.get(f"http://127.0.0.1:{port}/stop"): pass # Hold on to keep-alive connection. - await asyncio.sleep(5) + await asyncio.Event().wait() async def run_test(app: web.Application) -> None: nonlocal t @@ -1219,14 +1269,19 @@ async def run_test(app: web.Application) -> None: with contextlib.suppress(asyncio.CancelledError): await t - t = None + def on_ready(*args: Any, **kwargs: Any) -> None: + server_ready.set() + app = web.Application() app.cleanup_ctx.append(run_test) app.router.add_get("/stop", self.stop) - web.run_app(app, sock=sock, shutdown_timeout=10) - # If connection closed, then test() will be cancelled in cleanup_ctx. - # If not, then shutdown_timeout will allow it to sleep until complete. + start = time.monotonic() + web.run_app(app, sock=sock, shutdown_timeout=10, print=on_ready) + # Server should close idle keep-alive connections immediately on + # shutdown rather than waiting for shutdown_timeout to expire. + assert time.monotonic() - start < 5 + assert t is not None assert t.cancelled() def test_shutdown_close_websockets(self, unused_port_socket: socket.socket) -> None: @@ -1234,6 +1289,8 @@ def test_shutdown_close_websockets(self, unused_port_socket: socket.socket) -> N port = sock.getsockname()[1] WS = web.AppKey("ws", set[web.WebSocketResponse]) client_finished = server_finished = False + t = None + server_ready = asyncio.Event() async def ws_handler(request: web.Request) -> web.WebSocketResponse: ws = web.WebSocketResponse() @@ -1250,7 +1307,7 @@ async def close_websockets(app: web.Application) -> None: await ws.close(code=WSCloseCode.GOING_AWAY) async def test() -> None: - await asyncio.sleep(1) + await server_ready.wait() async with ClientSession() as sess: async with sess.ws_connect(f"http://127.0.0.1:{port}/ws") as ws: async with sess.get(f"http://127.0.0.1:{port}/stop"): @@ -1270,7 +1327,9 @@ async def run_test(app: web.Application) -> None: with contextlib.suppress(asyncio.CancelledError): await t - t = None + def on_ready(*args: Any, **kwargs: Any) -> None: + server_ready.set() + app = web.Application() app[WS] = set() app.on_shutdown.append(close_websockets) @@ -1279,7 +1338,7 @@ async def run_test(app: web.Application) -> None: app.router.add_get("/stop", self.stop) start = time.time() - web.run_app(app, sock=sock, shutdown_timeout=10) + web.run_app(app, sock=sock, shutdown_timeout=10, print=on_ready) assert time.time() - start < 5 assert client_finished assert server_finished @@ -1290,10 +1349,15 @@ def test_shutdown_handler_cancellation_suppressed( sock = unused_port_socket port = sock.getsockname()[1] actions = [] + t = None + server_ready = asyncio.Event() suppressed = asyncio.Event() + release_handler = asyncio.Event() async def test() -> None: - async def test_resp(sess): + await server_ready.wait() + + async def test_resp(sess: ClientSession) -> None: t = ClientTimeout(total=0.4) with pytest.raises(asyncio.TimeoutError): async with sess.get(f"http://127.0.0.1:{port}/", timeout=t) as resp: @@ -1321,20 +1385,34 @@ async def run_test(app: web.Application) -> None: async def handler(request: web.Request) -> web.Response: try: - await asyncio.sleep(5) + await asyncio.Event().wait() except asyncio.CancelledError: actions.append("SUPPRESSED") suppressed.set() - await asyncio.sleep(2) + await release_handler.wait() + await asyncio.sleep(0.5) actions.append("DONE") return web.Response(text="FOO") - t = None + async def on_shutdown(app: web.Application) -> None: + release_handler.set() + + def on_ready(*args: Any, **kwargs: Any) -> None: + server_ready.set() + app = web.Application() app.cleanup_ctx.append(run_test) + app.on_shutdown.append(on_shutdown) app.router.add_get("/", handler) app.router.add_get("/stop", self.stop) - web.run_app(app, sock=sock, shutdown_timeout=2, handler_cancellation=True) + web.run_app( + app, + sock=sock, + shutdown_timeout=2, + handler_cancellation=True, + print=on_ready, + ) + assert t is not None assert t.exception() is None assert actions == ["CANCELLED", "SUPPRESSED", "PRESTOP", "STOPPING", "DONE"] From f01bb0d1494ac6d657dc94af1a5efe097808c684 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 31 May 2026 22:46:45 +0100 Subject: [PATCH 173/191] [PR #12727/269f03b5 backport][3.14] add aiointercept to third party library list (#12749) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **This is a backport of PR #12727 as merged into master (269f03b572b9997d3e3a061cab36f259491d28e8).** Co-authored-by: Pablo Nicolás Estevez --- CHANGES/12727.doc.rst | 1 + docs/third_party.rst | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 CHANGES/12727.doc.rst diff --git a/CHANGES/12727.doc.rst b/CHANGES/12727.doc.rst new file mode 100644 index 00000000000..61ffeb82274 --- /dev/null +++ b/CHANGES/12727.doc.rst @@ -0,0 +1 @@ +Added ``aiointercept`` to list of third-party libraries -- by :user:`Polandia94`. diff --git a/docs/third_party.rst b/docs/third_party.rst index 12380d390fe..6188ab99d54 100644 --- a/docs/third_party.rst +++ b/docs/third_party.rst @@ -314,3 +314,6 @@ ask to raise the status. - `wireup `_ Performant, concise, and easy-to-use dependency injection container. + +- `aiointercept `_ + Mock aiohttp HTTP requests by routing them through a real aiohttp.web test server. From 660339a54b8cb166fd93e63a6c700682afb32ae9 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 31 May 2026 22:47:03 +0100 Subject: [PATCH 174/191] [PR #12726/e3a67148 backport][3.14] remove third party archived repos from docs (#12750) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **This is a backport of PR #12726 as merged into master (e3a67148e14f3fe6e098738ae9f7231de88e29ac).** Co-authored-by: Pablo Nicolás Estevez --- CHANGES/12726.doc.rst | 1 + docs/third_party.rst | 22 +++------------------- 2 files changed, 4 insertions(+), 19 deletions(-) create mode 100644 CHANGES/12726.doc.rst diff --git a/CHANGES/12726.doc.rst b/CHANGES/12726.doc.rst new file mode 100644 index 00000000000..45e1212acd1 --- /dev/null +++ b/CHANGES/12726.doc.rst @@ -0,0 +1 @@ +Removed archived and deprecated repositories from third party list -- by :user:`Polandia94`. diff --git a/docs/third_party.rst b/docs/third_party.rst index 6188ab99d54..5beafb48788 100644 --- a/docs/third_party.rst +++ b/docs/third_party.rst @@ -64,6 +64,9 @@ aiohttp extensions - `aiozipkin `_ distributed tracing instrumentation for `aiohttp` client and server. +- `aiocache `_ Caching for asyncio + with multiple backends (framework agnostic) + Database drivers ^^^^^^^^^^^^^^^^ @@ -146,8 +149,6 @@ We cannot vouch for the quality of these libraries, use them at your own risk. Please add your library reference here first and after some time ask to raise the status. -- `pytest-aiohttp-client `_ - Pytest fixture with simpler api, payload decoding and status code assertions. - `python-proxy-headers `_ provides ``aiohttp_proxy`` extension for receiving custom response headers from a proxy server @@ -161,18 +162,9 @@ ask to raise the status. - `aiohttp-cache `_ A cache system for aiohttp server. -- `aiocache `_ Caching for asyncio - with multiple backends (framework agnostic) - -- `gain `_ Web crawling framework - based on asyncio for everyone. - - `aiohttp-validate `_ Simple library that helps you validate your API endpoints requests/responses with json schema. -- `raven-aiohttp `_ An - aiohttp transport for raven-python (Sentry client). - - `webargs `_ A friendly library for parsing HTTP request arguments, with built-in support for popular web frameworks, including Flask, Django, Bottle, Tornado, @@ -188,14 +180,6 @@ ask to raise the status. - `aioresponses `_ a helper for mock/fake web requests in python aiohttp package. -- `aiohttp-transmute - `_ A transmute - implementation for aiohttp. - -- `aiohttp-login `_ - Registration and authorization (including social) for aiohttp - applications. - - `aiohttp_utils `_ Handy utilities for building aiohttp.web applications. From e0337acec2f3cc0458b63a01a7872086098ac3be Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 31 May 2026 23:47:45 +0100 Subject: [PATCH 175/191] [PR #12751/af3e9507 backport][3.14] Fix Dependabot Security (#12752) **This is a backport of PR #12751 as merged into master (af3e9507a5afa5630a4a77197d39fbb7a9f0e1ad).** Co-authored-by: Sam Bull --- requirements/pyproject.toml | 1 + 1 file changed, 1 insertion(+) create mode 120000 requirements/pyproject.toml diff --git a/requirements/pyproject.toml b/requirements/pyproject.toml new file mode 120000 index 00000000000..1e11d782571 --- /dev/null +++ b/requirements/pyproject.toml @@ -0,0 +1 @@ +../pyproject.toml \ No newline at end of file From 514567a27d3aebebd356167d29c9a956c9597128 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:55:04 +0100 Subject: [PATCH 176/191] [PR #12753/eeb21d08 backport][3.14] Improve ContentLengthError messages to show expected vs received bytes (#12755) **This is a backport of PR #12753 as merged into master (eeb21d0819b4d6dfb81faad060254915ec3c0794).** Co-authored-by: Sam Bull --- CHANGES/12753.misc.rst | 2 + aiohttp/_http_parser.pyx | 7 ++- aiohttp/http_parser.py | 5 +- tests/test_http_parser.py | 114 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 CHANGES/12753.misc.rst diff --git a/CHANGES/12753.misc.rst b/CHANGES/12753.misc.rst new file mode 100644 index 00000000000..0b73044f46a --- /dev/null +++ b/CHANGES/12753.misc.rst @@ -0,0 +1,2 @@ +Improved ``ContentLengthError`` exception messages to include both expected and received byte counts. This enhancement provides better diagnostics when debugging response body size mismatches +-- by :user:`bdraco` and :user:`Dreamsorcerer`. diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index 8c2141fe51f..8bf16ab16f4 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -325,6 +325,7 @@ cdef class HttpParser: bint _paused bint _eof_pending object _payload + unsigned long long _content_length_expected bint _payload_error object _payload_exception object _last_error @@ -363,6 +364,7 @@ cdef class HttpParser: cparser.llhttp_init(self._cparser, mode, self._csettings) self._cparser.data = self self._cparser.content_length = 0 + self._content_length_expected = 0 self.protocol = protocol self._loop = loop @@ -518,6 +520,7 @@ cdef class HttpParser: payload = EMPTY_PAYLOAD self._payload = payload + self._content_length_expected = self._cparser.content_length if encoding is not None and self._auto_decompress: self._payload = DeflateBuffer(payload, encoding, max_decompress_size=self._limit) @@ -561,8 +564,10 @@ cdef class HttpParser: raise TransferEncodingError( "Not enough data to satisfy transfer length header.") elif self._cparser.flags & cparser.F_CONTENT_LENGTH: + received = self._content_length_expected - self._cparser.content_length raise ContentLengthError( - "Not enough data to satisfy content length header.") + f"Not enough data to satisfy content length header " + f"(received {received} of {self._content_length_expected} bytes).") elif cparser.llhttp_get_errno(self._cparser) != cparser.HPE_OK: desc = cparser.llhttp_get_error_reason(self._cparser) raise PayloadEncodingError(desc.decode('latin-1')) diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py index 9e229cc4459..95a1a3aa8aa 100644 --- a/aiohttp/http_parser.py +++ b/aiohttp/http_parser.py @@ -859,6 +859,7 @@ def __init__( elif length is not None: self._type = ParseState.PARSE_LENGTH self._length = length + self._length_expected = length if self._length == 0: real_payload.feed_eof() self.done = True @@ -880,8 +881,10 @@ def feed_eof(self) -> None: self.done = True self._eof_pending = False elif self._type == ParseState.PARSE_LENGTH: + received = self._length_expected - self._length raise ContentLengthError( - "Not enough data to satisfy content length header." + f"Not enough data to satisfy content length header " + f"(received {received} of {self._length_expected} bytes)." ) elif self._type == ParseState.PARSE_CHUNKED: raise TransferEncodingError( diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index 5b5f3af6bc0..935cf65a493 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -2225,6 +2225,52 @@ async def test_parse_length_payload_eof(self, protocol: BaseProtocol) -> None: with pytest.raises(http_exceptions.ContentLengthError): p.feed_eof() + async def test_parse_length_payload_eof_error_message( + self, protocol: BaseProtocol + ) -> None: + """Test that ContentLengthError includes expected vs received bytes.""" + out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + + # Expect 10 bytes, but only send 3 + p = HttpPayloadParser(out, length=10, headers_parser=HeadersParser()) + p.feed_data(b"abc") + + with pytest.raises( + http_exceptions.ContentLengthError, match=r"received 3 of 10 bytes" + ): + p.feed_eof() + + async def test_parse_length_payload_eof_no_data( + self, protocol: BaseProtocol + ) -> None: + """Test ContentLengthError when no data is received.""" + out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + + # Expect 20 bytes, but send nothing + p = HttpPayloadParser(out, length=20, headers_parser=HeadersParser()) + + with pytest.raises( + http_exceptions.ContentLengthError, match=r"received 0 of 20 bytes" + ): + p.feed_eof() + + async def test_parse_length_payload_partial_data( + self, protocol: BaseProtocol + ) -> None: + """Test ContentLengthError with various amounts of partial data.""" + out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + + # Expect 100 bytes, but only send 45 + p = HttpPayloadParser(out, length=100, headers_parser=HeadersParser()) + p.feed_data(b"a" * 25) + p.feed_data(b"b" * 20) + + with pytest.raises( + http_exceptions.ContentLengthError, + match=r"received 45 of 100 bytes", + ): + p.feed_eof() + async def test_parse_chunked_payload_size_error( self, protocol: BaseProtocol ) -> None: @@ -2763,3 +2809,71 @@ async def test_streaming_decompress_large_payload( result = b"".join(buf._buffer) assert len(result) == len(original) assert result == original + + +def test_response_parser_incomplete_body_error_message( + response: HttpResponseParser, +) -> None: + """Test response parser error message for incomplete body.""" + # Response expects 50 bytes + response.feed_data(b"HTTP/1.1 200 OK\r\nContent-Length: 50\r\n\r\n") + # Send only 15 bytes + response.feed_data(b"partial content") + + with pytest.raises( + http_exceptions.ContentLengthError, match=r"received 15 of 50 bytes" + ): + response.feed_eof() + + +def test_response_parser_no_body_error_message(response: HttpResponseParser) -> None: + """Test response parser error when no body is received.""" + # Response expects 25 bytes + response.feed_data(b"HTTP/1.1 200 OK\r\nContent-Length: 25\r\n\r\n") + # Send no body data + + with pytest.raises( + http_exceptions.ContentLengthError, match=r"received 0 of 25 bytes" + ): + response.feed_eof() + + +def test_response_parser_partial_chunks_error_message( + response: HttpResponseParser, +) -> None: + """Test error message when body is sent in multiple chunks.""" + # Response expects 100 bytes + response.feed_data(b"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n") + # Send data in chunks totaling 60 bytes + response.feed_data(b"a" * 20) + response.feed_data(b"b" * 20) + response.feed_data(b"c" * 20) + + with pytest.raises( + http_exceptions.ContentLengthError, match=r"received 60 of 100 bytes" + ): + response.feed_eof() + + +def test_request_parser_incomplete_body_error_message( + parser: HttpRequestParser, +) -> None: + """Test request parser error message for incomplete body.""" + # Request with Content-Length but incomplete body + parser.feed_data(b"POST /test HTTP/1.1\r\nHost: a\r\nContent-Length: 30\r\n\r\n") + # Send only 10 bytes + parser.feed_data(b"incomplete") + + with pytest.raises( + http_exceptions.ContentLengthError, match=r"received 10 of 30 bytes" + ): + parser.feed_eof() + + +def test_response_content_length_zero_no_error(response: HttpResponseParser) -> None: + """Test that Content-Length: 0 does not raise error on feed_eof.""" + # Response with Content-Length: 0 + response.feed_data(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + + # This should NOT raise an error + response.feed_eof() # Should complete without exception From 236d2c147bf20fa91f89567d6ddb5c20895b5e34 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:20:45 +0100 Subject: [PATCH 177/191] [PR #12754/0d864ff9 backport][3.14] Fix sendfile test (#12756) **This is a backport of PR #12754 as merged into master (0d864ff9644205e5aa860e7598e4984aff6e07a9).** Co-authored-by: Sam Bull --- tests/test_web_sendfile.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/test_web_sendfile.py b/tests/test_web_sendfile.py index dbb2b902897..86bccb06d99 100644 --- a/tests/test_web_sendfile.py +++ b/tests/test_web_sendfile.py @@ -146,13 +146,19 @@ async def test_file_response_sends_headers_immediately() -> None: file_sender = FileResponse(filepath) file_sender._path = filepath - file_sender._sendfile = mock.AsyncMock(return_value=None) # type: ignore[method-assign] # FileResponse inherits from StreamResponse, so should send immediately assert file_sender._send_headers_immediately is True - # Prepare the response - await file_sender.prepare(request) + # Mock the actual transfer but let _sendfile call super().prepare() to write headers + with ( + mock.patch.object( + file_sender, "_sendfile_fallback", autospec=True, spec_set=True + ) as sendfile_fallback, + mock.patch("aiohttp.web_fileresponse.NOSENDFILE", True), + ): + sendfile_fallback.return_value = writer + await file_sender.prepare(request) # Headers should be sent immediately writer.send_headers.assert_called_once() From edb2724f481ba05ef889accc76bbe0089068e8eb Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 1 Jun 2026 19:21:38 +0100 Subject: [PATCH 178/191] Release v3.14.0 (#12757) --- CHANGES.rst | 692 ++++++++++++++++++++++++++++++++++ CHANGES/10142.bugfix.rst | 1 - CHANGES/10587.bugfix.rst | 1 - CHANGES/10600.bugfix.rst | 2 - CHANGES/10665.feature.rst | 1 - CHANGES/10683.bugfix.rst | 1 - CHANGES/10753.bugfix.rst | 1 - CHANGES/10785.misc.rst | 1 - CHANGES/10795.doc.rst | 4 - CHANGES/11601.breaking.rst | 1 - CHANGES/11681.feature.rst | 6 - CHANGES/11750.packaging.rst | 1 - CHANGES/11761.bugfix.rst | 4 - CHANGES/11763.feature.rst | 1 - CHANGES/11764.feature.rst | 1 - CHANGES/11766.feature.rst | 4 - CHANGES/11826.contrib.rst | 7 - CHANGES/11937.misc.rst | 2 - CHANGES/11966.feature.rst | 8 - CHANGES/11989.feature.rst | 7 - CHANGES/12011.bugfix.rst | 1 - CHANGES/12030.bugfix.rst | 2 - CHANGES/12091.bugfix.rst | 6 - CHANGES/12173.contrib.rst | 1 - CHANGES/12195.bugfix.rst | 2 - CHANGES/12234.bugfix.rst | 2 - CHANGES/12281.bugfix.rst | 1 - CHANGES/12312.bugfix.rst | 1 - CHANGES/12321.misc.rst | 2 - CHANGES/12349.contrib.rst | 1 - CHANGES/12358.misc.rst | 1 - CHANGES/12364.contrib.rst | 1 - CHANGES/12395.bugfix.rst | 4 - CHANGES/12406.contrib.rst | 2 - CHANGES/12436.bugfix.rst | 1 - CHANGES/12452.feature.rst | 2 - CHANGES/12493.bugfix | 3 - CHANGES/12499.deprecation.rst | 7 - CHANGES/12499.feature.rst | 3 - CHANGES/12512.misc.rst | 1 - CHANGES/12540.bugfix.rst | 1 - CHANGES/12549.doc.rst | 1 - CHANGES/12561.misc.rst | 2 - CHANGES/12562.contrib.rst | 1 - CHANGES/12569.misc.rst | 3 - CHANGES/12571.contrib.rst | 4 - CHANGES/12576.packaging.rst | 4 - CHANGES/12589.contrib.rst | 7 - CHANGES/12592.contrib.rst | 6 - CHANGES/12595.contrib.rst | 5 - CHANGES/12596.contrib.rst | 4 - CHANGES/12597.breaking.rst | 1 - CHANGES/12600.contrib.rst | 4 - CHANGES/12603.contrib.rst | 8 - CHANGES/12606.contrib.rst | 6 - CHANGES/12624.contrib.rst | 4 - CHANGES/12629.contrib.rst | 7 - CHANGES/12641.contrib.rst | 5 - CHANGES/12643.contrib.rst | 5 - CHANGES/12647.contrib.rst | 4 - CHANGES/12651.contrib.rst | 5 - CHANGES/12655.contrib.rst | 4 - CHANGES/12678.packaging.rst | 1 - CHANGES/12681.packaging.rst | 1 - CHANGES/12689.breaking.rst | 7 - CHANGES/12703.bugfix.rst | 1 - CHANGES/12706.bugfix.rst | 1 - CHANGES/12722.bugfix.rst | 1 - CHANGES/12726.doc.rst | 1 - CHANGES/12727.bugfix.rst | 1 - CHANGES/12727.doc.rst | 1 - CHANGES/12753.misc.rst | 2 - CHANGES/3951.feature.rst | 1 - CHANGES/7157.bugfix.rst | 6 - CHANGES/9308.breaking.rst | 12 - CHANGES/9705.contrib.rst | 2 - aiohttp/__init__.py | 2 +- 77 files changed, 693 insertions(+), 228 deletions(-) delete mode 100644 CHANGES/10142.bugfix.rst delete mode 100644 CHANGES/10587.bugfix.rst delete mode 100644 CHANGES/10600.bugfix.rst delete mode 100644 CHANGES/10665.feature.rst delete mode 100644 CHANGES/10683.bugfix.rst delete mode 100644 CHANGES/10753.bugfix.rst delete mode 100644 CHANGES/10785.misc.rst delete mode 100644 CHANGES/10795.doc.rst delete mode 100644 CHANGES/11601.breaking.rst delete mode 100644 CHANGES/11681.feature.rst delete mode 100644 CHANGES/11750.packaging.rst delete mode 100644 CHANGES/11761.bugfix.rst delete mode 100644 CHANGES/11763.feature.rst delete mode 120000 CHANGES/11764.feature.rst delete mode 100644 CHANGES/11766.feature.rst delete mode 100644 CHANGES/11826.contrib.rst delete mode 100644 CHANGES/11937.misc.rst delete mode 100644 CHANGES/11966.feature.rst delete mode 100644 CHANGES/11989.feature.rst delete mode 100644 CHANGES/12011.bugfix.rst delete mode 100644 CHANGES/12030.bugfix.rst delete mode 100644 CHANGES/12091.bugfix.rst delete mode 100644 CHANGES/12173.contrib.rst delete mode 100644 CHANGES/12195.bugfix.rst delete mode 100644 CHANGES/12234.bugfix.rst delete mode 100644 CHANGES/12281.bugfix.rst delete mode 100644 CHANGES/12312.bugfix.rst delete mode 100644 CHANGES/12321.misc.rst delete mode 100644 CHANGES/12349.contrib.rst delete mode 100644 CHANGES/12358.misc.rst delete mode 100644 CHANGES/12364.contrib.rst delete mode 100644 CHANGES/12395.bugfix.rst delete mode 100644 CHANGES/12406.contrib.rst delete mode 100644 CHANGES/12436.bugfix.rst delete mode 100644 CHANGES/12452.feature.rst delete mode 100644 CHANGES/12493.bugfix delete mode 100644 CHANGES/12499.deprecation.rst delete mode 100644 CHANGES/12499.feature.rst delete mode 100644 CHANGES/12512.misc.rst delete mode 100644 CHANGES/12540.bugfix.rst delete mode 100644 CHANGES/12549.doc.rst delete mode 100644 CHANGES/12561.misc.rst delete mode 100644 CHANGES/12562.contrib.rst delete mode 100644 CHANGES/12569.misc.rst delete mode 100644 CHANGES/12571.contrib.rst delete mode 100644 CHANGES/12576.packaging.rst delete mode 100644 CHANGES/12589.contrib.rst delete mode 100644 CHANGES/12592.contrib.rst delete mode 100644 CHANGES/12595.contrib.rst delete mode 100644 CHANGES/12596.contrib.rst delete mode 120000 CHANGES/12597.breaking.rst delete mode 100644 CHANGES/12600.contrib.rst delete mode 100644 CHANGES/12603.contrib.rst delete mode 100644 CHANGES/12606.contrib.rst delete mode 100644 CHANGES/12624.contrib.rst delete mode 100644 CHANGES/12629.contrib.rst delete mode 100644 CHANGES/12641.contrib.rst delete mode 100644 CHANGES/12643.contrib.rst delete mode 100644 CHANGES/12647.contrib.rst delete mode 100644 CHANGES/12651.contrib.rst delete mode 100644 CHANGES/12655.contrib.rst delete mode 100644 CHANGES/12678.packaging.rst delete mode 100644 CHANGES/12681.packaging.rst delete mode 100644 CHANGES/12689.breaking.rst delete mode 100644 CHANGES/12703.bugfix.rst delete mode 100644 CHANGES/12706.bugfix.rst delete mode 100644 CHANGES/12722.bugfix.rst delete mode 100644 CHANGES/12726.doc.rst delete mode 100644 CHANGES/12727.bugfix.rst delete mode 100644 CHANGES/12727.doc.rst delete mode 100644 CHANGES/12753.misc.rst delete mode 100644 CHANGES/3951.feature.rst delete mode 100644 CHANGES/7157.bugfix.rst delete mode 100644 CHANGES/9308.breaking.rst delete mode 100644 CHANGES/9705.contrib.rst diff --git a/CHANGES.rst b/CHANGES.rst index 2a7638f50a3..3f569fd5b45 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,698 @@ .. towncrier release notes start +3.14.0 (2026-06-01) +=================== + +We have a new website! https://aio-libs.org +Subscribe to the news feed to find out more about what we're working on in future. + +Features +-------- + +- Added ``RequestKey`` and ``ResponseKey`` classes, + which enable static type checking for request & response + context storages in the same way that ``AppKey`` does for ``Application`` + -- by :user:`gsoldatov`. + + + *Related issues and pull requests on GitHub:* + :issue:`11766`. + + + +- Added :func:`~aiohttp.encode_basic_auth` for encoding HTTP Basic + Authentication credentials. Replaces the now-deprecated + :class:`~aiohttp.BasicAuth` -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12499`. + + + +- Started accepting :term:`asynchronous context managers ` for cleanup contexts. + Legacy single-yield :term:`asynchronous generator` cleanup contexts continue to be + supported; async context managers are adapted internally so they are + entered at startup and exited during cleanup. + + -- by :user:`MannXo`. + + + *Related issues and pull requests on GitHub:* + :issue:`11681`. + + + +- Added :py:attr:`~aiohttp.CookieJar.cookies` and :py:attr:`~aiohttp.CookieJar.host_only_cookies` read-only properties to :py:class:`~aiohttp.CookieJar` exposing the stored cookies with their full attributes -- by :user:`Br1an67`. + + + *Related issues and pull requests on GitHub:* + :issue:`3951`. + + + +- Added :py:attr:`~aiohttp.web.TCPSite.port` accessor for dynamic port allocations in :class:`~aiohttp.web.TCPSite` -- by :user:`twhittock-disguise` and :user:`rodrigobnogueira`. + + + *Related issues and pull requests on GitHub:* + :issue:`10665`. + + + +- Added ``decode_text`` parameter to :meth:`~aiohttp.ClientSession.ws_connect` and :class:`~aiohttp.web.WebSocketResponse` to receive WebSocket TEXT messages as raw bytes instead of decoded strings, enabling direct use with high-performance JSON parsers like ``orjson`` -- by :user:`bdraco`. + + + *Related issues and pull requests on GitHub:* + :issue:`11763`, :issue:`11764`. + + + +- Large overhaul of parser/decompression code. + + The zip bomb security fix in 3.13 stopped highly compressed payloads + from being decompressed, regardless of validity. Now aiohttp will + decompress such payloads in chunks of 256+ KiB, allowing safe decompression + of such payloads. + + -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`11966`. + + + +- Added explicit APIs for bytes-returning JSON serializer: + ``JSONBytesEncoder`` type, ``JsonBytesPayload``, + :func:`~aiohttp.web.json_bytes_response`, + :meth:`~aiohttp.web.WebSocketResponse.send_json_bytes` and + :meth:`~aiohttp.ClientWebSocketResponse.send_json_bytes` methods, and + ``json_serialize_bytes`` parameter for :class:`~aiohttp.ClientSession` + -- by :user:`kevinpark1217`. + + + *Related issues and pull requests on GitHub:* + :issue:`11989`. + + + +- Added :attr:`~aiohttp.ClientResponse.output_size` and + :attr:`~aiohttp.ClientResponse.upload_complete` -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12452`. + + +Bug fixes +--------- + +- Fixed ``ZLibDecompressor`` silently dropping data past the first + member when decompressing concatenated gzip/deflate streams. Each subsequent + member is now handed to a fresh decompressor, matching the behaviour already + implemented for ZSTD multi-frame streams. + + -- by :user:`Ashutosh-177` + + + *Related issues and pull requests on GitHub:* + :issue:`7157`. + + + +- Improved the parser error message shown when TLS handshake bytes are received on an HTTP port -- by :user:`puneetdixit200`. + + + *Related issues and pull requests on GitHub:* + :issue:`10142`. + + + +- Fixed the C parser failing to reject a response with a body when none was expected -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`10587`. + + + +- Fixed http parser not rejecting HTTP/1.1 requests that do not have valid Host header. + -- by :user:`Cycloctane`. + + + *Related issues and pull requests on GitHub:* + :issue:`10600`. + + + +- Fixed misleading TLS-in-TLS warning being emitted when sending HTTPS requests through an HTTP proxy. The warning now only fires when the proxy itself uses HTTPS, which is the only case where TLS-in-TLS actually applies -- by :user:`wavebyrd`. + + + *Related issues and pull requests on GitHub:* + :issue:`10683`. + + + +- Fixed ``AssertionError`` when the transport is ``None`` during WebSocket + preparation or file response sending (e.g. when a client disconnects + immediately after connecting). A ``ConnectionResetError`` is now raised + instead -- by :user:`agners`. + + + *Related issues and pull requests on GitHub:* + :issue:`11761`. + + + +- Fixed ad-hoc cookies passed to individual requests not being sent when the session's cookie jar has ``unsafe=True`` and the target URL uses an IP address, by copying the ``unsafe`` setting from the session's cookie jar to the temporary cookie jar -- by :user:`Krishnachaitanyakc`. + + + *Related issues and pull requests on GitHub:* + :issue:`12011`. + + + +- Reset the WebSocket heartbeat timer on inbound data to avoid false ping/pong timeouts while receiving large frames + -- by :user:`hoffmang9`. + + + *Related issues and pull requests on GitHub:* + :issue:`12030`. + + + +- Switched :py:meth:`~aiohttp.CookieJar.save` to use JSON format and + :py:meth:`~aiohttp.CookieJar.load` to try JSON first with a fallback to + a restricted pickle unpickler -- by :user:`YuvalElbar6`. + + + *Related issues and pull requests on GitHub:* + :issue:`12091`. + + + +- Fixed redirects with consumed non-rewindable request bodies to raise + :class:`aiohttp.ClientPayloadError` instead of silently sending an empty body. + + + *Related issues and pull requests on GitHub:* + :issue:`12195`. + + + +- Fixed zstd decompression failing with ``ClientPayloadError`` when the server + sends a response as multiple zstd frames -- by :user:`josu-moreno`. + + + *Related issues and pull requests on GitHub:* + :issue:`12234`. + + + +- Fixed spurious ``Future exception was never retrieved`` warning on disconnect during back-pressure -- by :user:`availov`. + + + *Related issues and pull requests on GitHub:* + :issue:`12281`. + + + +- ``Cookiejar.save()`` now uses ``0x600`` permissions to better protect them from being read by other users -- by :user:`digiscrypt`. + + + *Related issues and pull requests on GitHub:* + :issue:`12312`. + + + +- Fixed a crash (:external+python:exc:`~http.cookies.CookieError`) in the cookie parser when receiving cookies + containing ASCII control characters on CPython builds with the :cve:`2026-3644` + patch. The parser now gracefully skips cookies whose value contains control + characters instead of letting the exception propagate -- by :user:`rodrigobnogueira`. + + + *Related issues and pull requests on GitHub:* + :issue:`12395`. + + + +- Fixed digest authentication failing for requests whose path or query string contains percent-encoded reserved characters; the digest signature now uses the encoded request-target that is sent on the wire instead of the decoded form -- by :user:`bdraco`. + + + *Related issues and pull requests on GitHub:* + :issue:`12436`. + + + +- Fixed :func:`aiohttp.web.run_app` losing inner traceback frames when an + exception is raised during application startup (e.g. inside + ``cleanup_ctx`` or ``on_startup``). Regression since 3.10.6. + + + *Related issues and pull requests on GitHub:* + :issue:`12493`. + + + +- Fixed per-request ``cookies`` not being dropped on cross-origin redirects -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12550`. + + + +- Fixed invalid bytes being allowed in multipart/payload headers -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12719`. + + + +- Fixed :py:meth:`~aiohttp.FormData.add_field` accepting invalid bytes in ``name`` and ``filename`` -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12721`. + + + +- Fixed websocket upgrade occurring when header contained a value like `notupgrade` -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12723`. + + + + +Deprecations (removal in next major release) +-------------------------------------------- + +- Deprecated :class:`~aiohttp.BasicAuth` and the ``auth`` / ``proxy_auth`` + parameters. They will be removed in aiohttp 4.0. Use the new + :func:`~aiohttp.encode_basic_auth` helper together with + ``headers={"Authorization": ...}`` (or + ``proxy_headers={"Proxy-Authorization": ...}`` for proxies) instead. + Note that ``encode_basic_auth()`` defaults to `utf-8`, not `latin1` + -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12499`. + + + +- Added deprecation warning to ``aiohttp.pytest_plugin``, please switch to ``pytest-aiohttp`` -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`10785`. + + + +Removals and backward incompatible breaking changes +--------------------------------------------------- + +- Stopped calling :func:`socket.getfqdn` as the fallback for + :attr:`aiohttp.web.BaseRequest.host`. :func:`socket.getfqdn` + performs blocking reverse DNS resolution on the event loop + thread and can stall a worker for many seconds when the system + resolver is slow, and could be triggered remotely by an HTTP/1.0 + request that omits the ``Host`` header. The fallback when no + ``Host`` header is present is now the local socket address the + request arrived on (transport ``sockname``), or an empty string + if no transport information is available. Code that relied on + the FQDN being returned must now read it from + :func:`socket.getfqdn` directly, off the event loop + -- by :user:`bdraco`. + + + *Related issues and pull requests on GitHub:* + :issue:`9308`, :issue:`12597`. + + + +- Dropped support for Python 3.9 -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`11601`. + + + +- Tightened outbound header serialization to reject all ASCII control + characters forbidden by :rfc:`9110#section-5.5` and :rfc:`9112#section-4` + (``0x00``\-``0x08``, ``0x0A``\-``0x1F``, ``0x7F``) in status lines, + header field-names, and field-values. Previously only CR, LF and NUL were + rejected. HTAB (``0x09``) remains permitted in field values. Applications + that placed bare control characters in outbound headers will now raise + :exc:`ValueError` instead of emitting non-RFC-compliant bytes -- by :user:`rodrigobnogueira`. + + + *Related issues and pull requests on GitHub:* + :issue:`12689`. + + + + +Improved documentation +---------------------- + +- Replaced the deprecated ``ujson`` library with ``orjson`` in the + client quickstart documentation. ``ujson`` has been put into + maintenance-only mode; ``orjson`` is the recommended alternative. + -- by :user:`indoor47` + + + *Related issues and pull requests on GitHub:* + :issue:`10795`. + + + +- Added the :doc:`threat_model` to the Sphinx documentation -- by :user:`omkar-334`. + + + *Related issues and pull requests on GitHub:* + :issue:`12549`. + + + +- Removed archived and deprecated repositories from third party list -- by :user:`Polandia94`. + + + *Related issues and pull requests on GitHub:* + :issue:`12726`. + + + +- Added ``aiointercept`` to list of third-party libraries -- by :user:`Polandia94`. + + + *Related issues and pull requests on GitHub:* + :issue:`12727`. + + + + +Packaging updates and notes for downstreams +------------------------------------------- + +- Added wheels for Android and iOS platforms -- by :user:`timrid`. + + + *Related issues and pull requests on GitHub:* + :issue:`11750`. + + + +- Parallelized the Cython extension compilation by defaulting + ``build_ext.parallel`` to ``os.cpu_count()``, so each module's + ``gcc`` invocation now runs concurrently instead of one at a time + -- by :user:`bdraco`. + + + *Related issues and pull requests on GitHub:* + :issue:`12576`. + + + +- Submitted vendored `llhttp` to Github's SBOM -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12678`. + + + +- Updated ``llhttp`` to v9.4.1 -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12681`. + + + + +Contributor-facing changes +-------------------------- + +- The coverage tool is now configured using the new native + auto-discovered :file:`.coveragerc.toml` file + -- by :user:`webknjaz`. + + It is also set up to use the ``ctrace`` core that works + around the performance issues in the ``sysmon`` tracer + which is default under Python 3.14. + + + *Related issues and pull requests on GitHub:* + :issue:`11826`. + + + +- Fixed and reworked ``autobahn`` tests -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12173`. + + + +- Added a CI job to measure Cython coverage -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12349`. + + + +- Disabled ``coverage`` and ``xdist`` by default to ease local development -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12364`. + + + +- Avoid installation of backports.zstd on Python 3.14 in linting dependency set + -- by :user:`seifertm`. + + + *Related issues and pull requests on GitHub:* + :issue:`12406`. + + + +- Added ``--durations=30`` to the benchmark CI run so the slowest tests are reported when the job hits its timeout -- by :user:`aiolibsbot`. + + + *Related issues and pull requests on GitHub:* + :issue:`12562`. + + + +- Fixed two flakey ``test_middleware_uses_session_avoids_recursion_with_*`` tests + that hard coded ``localhost`` in the inner middleware request; they now target + the bound server URL so happy eyeballs cannot pick an unbound address on + Windows runners -- by :user:`bdraco`. + + + *Related issues and pull requests on GitHub:* + :issue:`12571`. + + + +- Restricted the ``isal`` test dependency to CPython, since + ``isal`` 1.8.0 stopped publishing PyPy wheels and the source + build requires ``nasm``, which is not available on the CI + runners. The ``parametrize_zlib_backend`` fixture already + calls ``pytest.importorskip``, so PyPy continues to exercise + the ``zlib`` and ``zlib_ng`` backends with no further + changes -- by :user:`bdraco`. + + + *Related issues and pull requests on GitHub:* + :issue:`12589`. + + + +- Fixed a flakey ``test_tcp_connector_fingerprint_ok`` by aborting + the SSL shutdown on the test's TCP connector before returning. + The graceful TLS close was occasionally outliving the test event + loop on one of the CI jobs, and the teardown ``gc.collect()`` + then surfaced the still-open transport as a + ``PytestUnraisableExceptionWarning`` -- by :user:`bdraco`. + + + *Related issues and pull requests on GitHub:* + :issue:`12592`. + + + +- Switched the ``cibuildwheel`` build frontend to ``build[uv]`` so + that ``uv`` provisions every build-isolation virtual environment + in the wheel matrix, replacing the per-ABI ``pip`` resolve with a + roughly sub-second ``uv`` resolve + -- by :user:`bdraco`. + + + *Related issues and pull requests on GitHub:* + :issue:`12595`. + + + +- Fixed flaky ``test_handler_returns_not_response`` and + ``test_handler_returns_none`` by routing ``loop.set_debug(True)`` + through a new ``loop_debug_mode`` fixture that disables debug + mode before the ``aiohttp_client`` fixture finalizes. Leaving + debug on through teardown let PyPy 3.11's asyncio slow-callback + logger walk into ``Task.__repr__`` during connector close, + surfacing a spurious ``RuntimeWarning: coroutine was never + awaited`` -- by :user:`bdraco`. + + + *Related issues and pull requests on GitHub:* + :issue:`12603`. + + + +- Reduced runtime of several of the slowest unit tests + (decompress size-limit payloads from 64 MiB to 2 MiB, + ``test_chunk_splits_after_pause`` chunk count from 50000 + to 20000, and ``test_set_cookies_max_age`` sleep from 2 + seconds to 1.1 seconds) without changing what they + exercise -- by :user:`bdraco`. + + + *Related issues and pull requests on GitHub:* + :issue:`12606`. + + + +- Added a default 120-second per-test timeout via ``pytest-timeout`` so a + hung test surfaces by name in CI output instead of getting hidden behind + the job-level timeout added in :pr:`12619`. The ``autobahn`` and + benchmark jobs opt out with ``--timeout=0`` -- by :user:`bdraco`. + + + *Related issues and pull requests on GitHub:* + :issue:`12624`. + + + +- Switched the CI ``test`` and ``autobahn`` jobs from + ``actions/setup-python`` to ``astral-sh/setup-uv`` for installing + interpreters, cutting the ``Setup Python`` step from 40-58s to a + few seconds on ``macos-latest`` and ``windows-latest`` runners for + variants not in the hosted tool-cache (notably the free-threaded + ``3.14t``) + -- by :user:`bdraco`. + + + *Related issues and pull requests on GitHub:* + :issue:`12629`. + + + +- Made the ``pip`` command used by the :file:`Makefile` configurable via a + ``PIP`` variable; downstream consumers can now run, for example, + ``make .develop PIP="uv pip"`` to install via ``uv`` without us + maintaining a parallel target + -- by :user:`bdraco`. + + + *Related issues and pull requests on GitHub:* + :issue:`12641`. + + + +- Allowed re-running the ``deploy`` job in ``.github/workflows/ci-cd.yml`` + after a partial release failure: the ``Make Release`` step now skips + when the GitHub Release already exists, and the PyPI publish step uses + ``skip-existing`` so dists that were already uploaded on a prior + attempt do not break the retry -- by :user:`bdraco`. + + + *Related issues and pull requests on GitHub:* + :issue:`12651`. + + + +- Switched the armv7l wheel builds onto GitHub's hosted ARM runners. The + 32-bit ARM build still runs under QEMU, but the host is now aarch64 + rather than x86_64, so the emulation overhead drops sharply + -- by :user:`bdraco`. + + + *Related issues and pull requests on GitHub:* + :issue:`12655`. + + + + +Miscellaneous internal changes +------------------------------ + +- Added win_arm64 to the wheels that gets pushed to PyPI + -- by :user:`AraHaan`. + + + *Related issues and pull requests on GitHub:* + :issue:`11937`. + + + +- Added ``cdef`` type declarations and inlined the upgrade check in the HTTP parser + -- by :user:`bdraco`. + + + *Related issues and pull requests on GitHub:* + :issue:`12321`. + + + +- Changed ``zlib_executor_size`` default so compressed payloads are async by default -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12358`. + + + +- Added ``THREAT_MODEL.md`` detailing our security stance -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12512`. + + + +- Reduced payload sizes and request counts in the slowest client and URL + dispatcher benchmarks so they no longer dominate CI runtime + -- by :user:`bdraco`. + + + *Related issues and pull requests on GitHub:* + :issue:`12569`. + + + +- Improved ``ContentLengthError`` exception messages to include both expected and received byte counts. This enhancement provides better diagnostics when debugging response body size mismatches + -- by :user:`bdraco` and :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12753`. + + + + +---- + + 3.13.5 (2026-03-31) =================== diff --git a/CHANGES/10142.bugfix.rst b/CHANGES/10142.bugfix.rst deleted file mode 100644 index 821db59818d..00000000000 --- a/CHANGES/10142.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Improved the parser error message shown when TLS handshake bytes are received on an HTTP port -- by :user:`puneetdixit200`. diff --git a/CHANGES/10587.bugfix.rst b/CHANGES/10587.bugfix.rst deleted file mode 100644 index f60e1d17c5f..00000000000 --- a/CHANGES/10587.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed the C parser failing to reject a response with a body when none was expected -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/10600.bugfix.rst b/CHANGES/10600.bugfix.rst deleted file mode 100644 index eba47bf56e6..00000000000 --- a/CHANGES/10600.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fixed http parser not rejecting HTTP/1.1 requests that do not have valid Host header. --- by :user:`Cycloctane`. diff --git a/CHANGES/10665.feature.rst b/CHANGES/10665.feature.rst deleted file mode 100644 index afb4768c7cf..00000000000 --- a/CHANGES/10665.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Added :py:attr:`~aiohttp.web.TCPSite.port` accessor for dynamic port allocations in :class:`~aiohttp.web.TCPSite` -- by :user:`twhittock-disguise` and :user:`rodrigobnogueira`. diff --git a/CHANGES/10683.bugfix.rst b/CHANGES/10683.bugfix.rst deleted file mode 100644 index 9631cc5fa05..00000000000 --- a/CHANGES/10683.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed misleading TLS-in-TLS warning being emitted when sending HTTPS requests through an HTTP proxy. The warning now only fires when the proxy itself uses HTTPS, which is the only case where TLS-in-TLS actually applies -- by :user:`wavebyrd`. diff --git a/CHANGES/10753.bugfix.rst b/CHANGES/10753.bugfix.rst deleted file mode 100644 index e0f4cfd0dd1..00000000000 --- a/CHANGES/10753.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Widened ``trace_request_ctx`` parameter type from ``Mapping[str, Any] | None`` to ``object`` to allow passing instances of user-defined classes as trace context -- by :user:`nightcityblade`. diff --git a/CHANGES/10785.misc.rst b/CHANGES/10785.misc.rst deleted file mode 100644 index 5b43f63a42d..00000000000 --- a/CHANGES/10785.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Added deprecation warning to ``aiohttp.pytest_plugin``, please switch to ``pytest-aiohttp`` -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/10795.doc.rst b/CHANGES/10795.doc.rst deleted file mode 100644 index 91094f70c13..00000000000 --- a/CHANGES/10795.doc.rst +++ /dev/null @@ -1,4 +0,0 @@ -Replaced the deprecated ``ujson`` library with ``orjson`` in the -client quickstart documentation. ``ujson`` has been put into -maintenance-only mode; ``orjson`` is the recommended alternative. --- by :user:`indoor47` diff --git a/CHANGES/11601.breaking.rst b/CHANGES/11601.breaking.rst deleted file mode 100644 index c2eccbd9e1c..00000000000 --- a/CHANGES/11601.breaking.rst +++ /dev/null @@ -1 +0,0 @@ -Dropped support for Python 3.9 -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/11681.feature.rst b/CHANGES/11681.feature.rst deleted file mode 100644 index 21b0ab1f7c7..00000000000 --- a/CHANGES/11681.feature.rst +++ /dev/null @@ -1,6 +0,0 @@ -Started accepting :term:`asynchronous context managers ` for cleanup contexts. -Legacy single-yield :term:`asynchronous generator` cleanup contexts continue to be -supported; async context managers are adapted internally so they are -entered at startup and exited during cleanup. - --- by :user:`MannXo`. diff --git a/CHANGES/11750.packaging.rst b/CHANGES/11750.packaging.rst deleted file mode 100644 index 2af5e11cf45..00000000000 --- a/CHANGES/11750.packaging.rst +++ /dev/null @@ -1 +0,0 @@ -Added wheels for Android and iOS platforms -- by :user:`timrid`. diff --git a/CHANGES/11761.bugfix.rst b/CHANGES/11761.bugfix.rst deleted file mode 100644 index d4661c6d4a1..00000000000 --- a/CHANGES/11761.bugfix.rst +++ /dev/null @@ -1,4 +0,0 @@ -Fixed ``AssertionError`` when the transport is ``None`` during WebSocket -preparation or file response sending (e.g. when a client disconnects -immediately after connecting). A ``ConnectionResetError`` is now raised -instead -- by :user:`agners`. diff --git a/CHANGES/11763.feature.rst b/CHANGES/11763.feature.rst deleted file mode 100644 index b34bfafaca8..00000000000 --- a/CHANGES/11763.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Added ``decode_text`` parameter to :meth:`~aiohttp.ClientSession.ws_connect` and :class:`~aiohttp.web.WebSocketResponse` to receive WebSocket TEXT messages as raw bytes instead of decoded strings, enabling direct use with high-performance JSON parsers like ``orjson`` -- by :user:`bdraco`. diff --git a/CHANGES/11764.feature.rst b/CHANGES/11764.feature.rst deleted file mode 120000 index 0860becd808..00000000000 --- a/CHANGES/11764.feature.rst +++ /dev/null @@ -1 +0,0 @@ -11763.feature.rst \ No newline at end of file diff --git a/CHANGES/11766.feature.rst b/CHANGES/11766.feature.rst deleted file mode 100644 index de57ca44543..00000000000 --- a/CHANGES/11766.feature.rst +++ /dev/null @@ -1,4 +0,0 @@ -Added ``RequestKey`` and ``ResponseKey`` classes, -which enable static type checking for request & response -context storages in the same way that ``AppKey`` does for ``Application`` --- by :user:`gsoldatov`. diff --git a/CHANGES/11826.contrib.rst b/CHANGES/11826.contrib.rst deleted file mode 100644 index 134eda601c2..00000000000 --- a/CHANGES/11826.contrib.rst +++ /dev/null @@ -1,7 +0,0 @@ -The coverage tool is now configured using the new native -auto-discovered :file:`.coveragerc.toml` file --- by :user:`webknjaz`. - -It is also set up to use the ``ctrace`` core that works -around the performance issues in the ``sysmon`` tracer -which is default under Python 3.14. diff --git a/CHANGES/11937.misc.rst b/CHANGES/11937.misc.rst deleted file mode 100644 index e8435d14618..00000000000 --- a/CHANGES/11937.misc.rst +++ /dev/null @@ -1,2 +0,0 @@ -Added win_arm64 to the wheels that gets pushed to PyPI --- by :user:`AraHaan`. diff --git a/CHANGES/11966.feature.rst b/CHANGES/11966.feature.rst deleted file mode 100644 index 9f298f98e12..00000000000 --- a/CHANGES/11966.feature.rst +++ /dev/null @@ -1,8 +0,0 @@ -Large overhaul of parser/decompression code. - -The zip bomb security fix in 3.13 stopped highly compressed payloads -from being decompressed, regardless of validity. Now aiohttp will -decompress such payloads in chunks of 256+ KiB, allowing safe decompression -of such payloads. - --- by :user:`Dreamsorcerer`. diff --git a/CHANGES/11989.feature.rst b/CHANGES/11989.feature.rst deleted file mode 100644 index ced05b5e100..00000000000 --- a/CHANGES/11989.feature.rst +++ /dev/null @@ -1,7 +0,0 @@ -Added explicit APIs for bytes-returning JSON serializer: -``JSONBytesEncoder`` type, ``JsonBytesPayload``, -:func:`~aiohttp.web.json_bytes_response`, -:meth:`~aiohttp.web.WebSocketResponse.send_json_bytes` and -:meth:`~aiohttp.ClientWebSocketResponse.send_json_bytes` methods, and -``json_serialize_bytes`` parameter for :class:`~aiohttp.ClientSession` --- by :user:`kevinpark1217`. diff --git a/CHANGES/12011.bugfix.rst b/CHANGES/12011.bugfix.rst deleted file mode 100644 index 701a2ed01e3..00000000000 --- a/CHANGES/12011.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed ad-hoc cookies passed to individual requests not being sent when the session's cookie jar has ``unsafe=True`` and the target URL uses an IP address, by copying the ``unsafe`` setting from the session's cookie jar to the temporary cookie jar -- by :user:`Krishnachaitanyakc`. diff --git a/CHANGES/12030.bugfix.rst b/CHANGES/12030.bugfix.rst deleted file mode 100644 index 5f2f8ba5c3c..00000000000 --- a/CHANGES/12030.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Reset the WebSocket heartbeat timer on inbound data to avoid false ping/pong timeouts while receiving large frames --- by :user:`hoffmang9`. diff --git a/CHANGES/12091.bugfix.rst b/CHANGES/12091.bugfix.rst deleted file mode 100644 index 45ffbc3557f..00000000000 --- a/CHANGES/12091.bugfix.rst +++ /dev/null @@ -1,6 +0,0 @@ -Switched :py:meth:`~aiohttp.CookieJar.save` to use JSON format and -:py:meth:`~aiohttp.CookieJar.load` to try JSON first with a fallback to -a restricted pickle unpickler that only allows cookie-related types -(``SimpleCookie``, ``Morsel``, ``defaultdict``, etc.), preventing -arbitrary code execution via malicious pickle payloads -(CWE-502) -- by :user:`YuvalElbar6`. diff --git a/CHANGES/12173.contrib.rst b/CHANGES/12173.contrib.rst deleted file mode 100644 index 7c1bd3d1c1b..00000000000 --- a/CHANGES/12173.contrib.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed and reworked ``autobahn`` tests -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12195.bugfix.rst b/CHANGES/12195.bugfix.rst deleted file mode 100644 index 34729b52f0d..00000000000 --- a/CHANGES/12195.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fixed redirects with consumed non-rewindable request bodies to raise -:class:`aiohttp.ClientPayloadError` instead of silently sending an empty body. diff --git a/CHANGES/12234.bugfix.rst b/CHANGES/12234.bugfix.rst deleted file mode 100644 index 64bcfa24f69..00000000000 --- a/CHANGES/12234.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fixed zstd decompression failing with ``ClientPayloadError`` when the server -sends a response as multiple zstd frames -- by :user:`josu-moreno`. diff --git a/CHANGES/12281.bugfix.rst b/CHANGES/12281.bugfix.rst deleted file mode 100644 index 63521a73b1c..00000000000 --- a/CHANGES/12281.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed spurious ``Future exception was never retrieved`` warning on disconnect during back-pressure -- by :user:`availov`. diff --git a/CHANGES/12312.bugfix.rst b/CHANGES/12312.bugfix.rst deleted file mode 100644 index a7d240ad79c..00000000000 --- a/CHANGES/12312.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -``Cookiejar.save()`` now uses ``0x600`` permissions to better protect them from being read by other users -- by :user:`digiscrypt`. diff --git a/CHANGES/12321.misc.rst b/CHANGES/12321.misc.rst deleted file mode 100644 index 71f9db77675..00000000000 --- a/CHANGES/12321.misc.rst +++ /dev/null @@ -1,2 +0,0 @@ -Added ``cdef`` type declarations and inlined the upgrade check in the HTTP parser --- by :user:`bdraco`. diff --git a/CHANGES/12349.contrib.rst b/CHANGES/12349.contrib.rst deleted file mode 100644 index 12aeb069354..00000000000 --- a/CHANGES/12349.contrib.rst +++ /dev/null @@ -1 +0,0 @@ -Added a CI job to measure Cython coverage -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12358.misc.rst b/CHANGES/12358.misc.rst deleted file mode 100644 index 7b035023500..00000000000 --- a/CHANGES/12358.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Changed ``zlib_executor_size`` default so compressed payloads are async by default -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12364.contrib.rst b/CHANGES/12364.contrib.rst deleted file mode 100644 index 21b9eb1b271..00000000000 --- a/CHANGES/12364.contrib.rst +++ /dev/null @@ -1 +0,0 @@ -Disabled ``coverage`` and ``xdist`` by default to ease local development -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12395.bugfix.rst b/CHANGES/12395.bugfix.rst deleted file mode 100644 index e3c67bfa7aa..00000000000 --- a/CHANGES/12395.bugfix.rst +++ /dev/null @@ -1,4 +0,0 @@ -Fixed a crash (:external+python:exc:`~http.cookies.CookieError`) in the cookie parser when receiving cookies -containing ASCII control characters on CPython builds with the :cve:`2026-3644` -patch. The parser now gracefully skips cookies whose value contains control -characters instead of letting the exception propagate -- by :user:`rodrigobnogueira`. diff --git a/CHANGES/12406.contrib.rst b/CHANGES/12406.contrib.rst deleted file mode 100644 index 9bcbee91e09..00000000000 --- a/CHANGES/12406.contrib.rst +++ /dev/null @@ -1,2 +0,0 @@ -Avoid installation of backports.zstd on Python 3.14 in linting dependency set --- by :user:`seifertm`. diff --git a/CHANGES/12436.bugfix.rst b/CHANGES/12436.bugfix.rst deleted file mode 100644 index d6f7e160697..00000000000 --- a/CHANGES/12436.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed digest authentication failing for requests whose path or query string contains percent-encoded reserved characters; the digest signature now uses the encoded request-target that is sent on the wire instead of the decoded form -- by :user:`bdraco`. diff --git a/CHANGES/12452.feature.rst b/CHANGES/12452.feature.rst deleted file mode 100644 index ad4936323da..00000000000 --- a/CHANGES/12452.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -Added :attr:`~aiohttp.ClientResponse.output_size` and -:attr:`~aiohttp.ClientResponse.upload_complete` -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12493.bugfix b/CHANGES/12493.bugfix deleted file mode 100644 index 7a68daec0ba..00000000000 --- a/CHANGES/12493.bugfix +++ /dev/null @@ -1,3 +0,0 @@ -Fixed :func:`aiohttp.web.run_app` losing inner traceback frames when an -exception is raised during application startup (e.g. inside -``cleanup_ctx`` or ``on_startup``). Regression since 3.10.6. diff --git a/CHANGES/12499.deprecation.rst b/CHANGES/12499.deprecation.rst deleted file mode 100644 index c9a5b533c77..00000000000 --- a/CHANGES/12499.deprecation.rst +++ /dev/null @@ -1,7 +0,0 @@ -Deprecated :class:`~aiohttp.BasicAuth` and the ``auth`` / ``proxy_auth`` -parameters. They will be removed in aiohttp 4.0. Use the new -:func:`~aiohttp.encode_basic_auth` helper together with -``headers={"Authorization": ...}`` (or -``proxy_headers={"Proxy-Authorization": ...}`` for proxies) instead. -Note that ``encode_basic_auth()`` defaults to `utf-8`, not `latin1` --- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12499.feature.rst b/CHANGES/12499.feature.rst deleted file mode 100644 index 8b242432367..00000000000 --- a/CHANGES/12499.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -Added :func:`~aiohttp.encode_basic_auth` for encoding HTTP Basic -Authentication credentials. Replaces the now-deprecated -:class:`~aiohttp.BasicAuth` -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12512.misc.rst b/CHANGES/12512.misc.rst deleted file mode 100644 index c3acbcce786..00000000000 --- a/CHANGES/12512.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Added ``THREAT_MODEL.md`` detailing our security stance -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12540.bugfix.rst b/CHANGES/12540.bugfix.rst deleted file mode 100644 index dfd98129e4b..00000000000 --- a/CHANGES/12540.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed per-request ``cookies`` not being dropped on cross-origin redirects -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12549.doc.rst b/CHANGES/12549.doc.rst deleted file mode 100644 index 0288c33cba5..00000000000 --- a/CHANGES/12549.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Added the :doc:`threat_model` to the Sphinx documentation -- by :user:`omkar-334`. diff --git a/CHANGES/12561.misc.rst b/CHANGES/12561.misc.rst deleted file mode 100644 index 4201bb90b0d..00000000000 --- a/CHANGES/12561.misc.rst +++ /dev/null @@ -1,2 +0,0 @@ -Bumped the benchmark CI job timeout from 12 to 15 minutes to prevent -spurious failures on slower runners -- by :user:`aiolibsbot`. diff --git a/CHANGES/12562.contrib.rst b/CHANGES/12562.contrib.rst deleted file mode 100644 index 0d89212d365..00000000000 --- a/CHANGES/12562.contrib.rst +++ /dev/null @@ -1 +0,0 @@ -Added ``--durations=30`` to the benchmark CI run so the slowest tests are reported when the job hits its timeout -- by :user:`aiolibsbot`. diff --git a/CHANGES/12569.misc.rst b/CHANGES/12569.misc.rst deleted file mode 100644 index 32af3f259b4..00000000000 --- a/CHANGES/12569.misc.rst +++ /dev/null @@ -1,3 +0,0 @@ -Reduced payload sizes and request counts in the slowest client and URL -dispatcher benchmarks so they no longer dominate CI runtime --- by :user:`bdraco`. diff --git a/CHANGES/12571.contrib.rst b/CHANGES/12571.contrib.rst deleted file mode 100644 index 8fb1b309375..00000000000 --- a/CHANGES/12571.contrib.rst +++ /dev/null @@ -1,4 +0,0 @@ -Fixed two flakey ``test_middleware_uses_session_avoids_recursion_with_*`` tests -that hard coded ``localhost`` in the inner middleware request; they now target -the bound server URL so happy eyeballs cannot pick an unbound address on -Windows runners -- by :user:`bdraco`. diff --git a/CHANGES/12576.packaging.rst b/CHANGES/12576.packaging.rst deleted file mode 100644 index dc748bfe726..00000000000 --- a/CHANGES/12576.packaging.rst +++ /dev/null @@ -1,4 +0,0 @@ -Parallelized the Cython extension compilation by defaulting -``build_ext.parallel`` to ``os.cpu_count()``, so each module's -``gcc`` invocation now runs concurrently instead of one at a time --- by :user:`bdraco`. diff --git a/CHANGES/12589.contrib.rst b/CHANGES/12589.contrib.rst deleted file mode 100644 index dc7f6400d57..00000000000 --- a/CHANGES/12589.contrib.rst +++ /dev/null @@ -1,7 +0,0 @@ -Restricted the ``isal`` test dependency to CPython, since -``isal`` 1.8.0 stopped publishing PyPy wheels and the source -build requires ``nasm``, which is not available on the CI -runners. The ``parametrize_zlib_backend`` fixture already -calls ``pytest.importorskip``, so PyPy continues to exercise -the ``zlib`` and ``zlib_ng`` backends with no further -changes -- by :user:`bdraco`. diff --git a/CHANGES/12592.contrib.rst b/CHANGES/12592.contrib.rst deleted file mode 100644 index 76d2f8b7035..00000000000 --- a/CHANGES/12592.contrib.rst +++ /dev/null @@ -1,6 +0,0 @@ -Fixed a flakey ``test_tcp_connector_fingerprint_ok`` by aborting -the SSL shutdown on the test's TCP connector before returning. -The graceful TLS close was occasionally outliving the test event -loop on one of the CI jobs, and the teardown ``gc.collect()`` -then surfaced the still-open transport as a -``PytestUnraisableExceptionWarning`` -- by :user:`bdraco`. diff --git a/CHANGES/12595.contrib.rst b/CHANGES/12595.contrib.rst deleted file mode 100644 index 54af2262fb1..00000000000 --- a/CHANGES/12595.contrib.rst +++ /dev/null @@ -1,5 +0,0 @@ -Switched the ``cibuildwheel`` build frontend to ``build[uv]`` so -that ``uv`` provisions every build-isolation virtual environment -in the wheel matrix, replacing the per-ABI ``pip`` resolve with a -roughly sub-second ``uv`` resolve --- by :user:`bdraco`. diff --git a/CHANGES/12596.contrib.rst b/CHANGES/12596.contrib.rst deleted file mode 100644 index ff96abe5bfc..00000000000 --- a/CHANGES/12596.contrib.rst +++ /dev/null @@ -1,4 +0,0 @@ -Switched the test-results upload step to ``codecov/codecov-action@v6`` -with ``report_type: test_results``, since -``codecov/test-results-action`` is being deprecated in favor of -``codecov-action`` -- by :user:`bdraco`. diff --git a/CHANGES/12597.breaking.rst b/CHANGES/12597.breaking.rst deleted file mode 120000 index 651891d5065..00000000000 --- a/CHANGES/12597.breaking.rst +++ /dev/null @@ -1 +0,0 @@ -9308.breaking.rst \ No newline at end of file diff --git a/CHANGES/12600.contrib.rst b/CHANGES/12600.contrib.rst deleted file mode 100644 index 4d361389191..00000000000 --- a/CHANGES/12600.contrib.rst +++ /dev/null @@ -1,4 +0,0 @@ -Silenced the ``DeprecationWarning`` raised by ``uvloop`` 0.22+ when it -accesses the ``asyncio.AbstractEventLoopPolicy`` alias that Python 3.14 -marks for removal in 3.16, so the test suite passes against the newer -``uvloop`` release -- by :user:`bdraco`. diff --git a/CHANGES/12603.contrib.rst b/CHANGES/12603.contrib.rst deleted file mode 100644 index 51574d02868..00000000000 --- a/CHANGES/12603.contrib.rst +++ /dev/null @@ -1,8 +0,0 @@ -Fixed flaky ``test_handler_returns_not_response`` and -``test_handler_returns_none`` by routing ``loop.set_debug(True)`` -through a new ``loop_debug_mode`` fixture that disables debug -mode before the ``aiohttp_client`` fixture finalizes. Leaving -debug on through teardown let PyPy 3.11's asyncio slow-callback -logger walk into ``Task.__repr__`` during connector close, -surfacing a spurious ``RuntimeWarning: coroutine was never -awaited`` -- by :user:`bdraco`. diff --git a/CHANGES/12606.contrib.rst b/CHANGES/12606.contrib.rst deleted file mode 100644 index 72925114e8c..00000000000 --- a/CHANGES/12606.contrib.rst +++ /dev/null @@ -1,6 +0,0 @@ -Reduced runtime of several of the slowest unit tests -(decompress size-limit payloads from 64 MiB to 2 MiB, -``test_chunk_splits_after_pause`` chunk count from 50000 -to 20000, and ``test_set_cookies_max_age`` sleep from 2 -seconds to 1.1 seconds) without changing what they -exercise -- by :user:`bdraco`. diff --git a/CHANGES/12624.contrib.rst b/CHANGES/12624.contrib.rst deleted file mode 100644 index 0c7e2a0a548..00000000000 --- a/CHANGES/12624.contrib.rst +++ /dev/null @@ -1,4 +0,0 @@ -Added a default 120-second per-test timeout via ``pytest-timeout`` so a -hung test surfaces by name in CI output instead of getting hidden behind -the job-level timeout added in :pr:`12619`. The ``autobahn`` and -benchmark jobs opt out with ``--timeout=0`` -- by :user:`bdraco`. diff --git a/CHANGES/12629.contrib.rst b/CHANGES/12629.contrib.rst deleted file mode 100644 index 8c10b198425..00000000000 --- a/CHANGES/12629.contrib.rst +++ /dev/null @@ -1,7 +0,0 @@ -Switched the CI ``test`` and ``autobahn`` jobs from -``actions/setup-python`` to ``astral-sh/setup-uv`` for installing -interpreters, cutting the ``Setup Python`` step from 40-58s to a -few seconds on ``macos-latest`` and ``windows-latest`` runners for -variants not in the hosted tool-cache (notably the free-threaded -``3.14t``) --- by :user:`bdraco`. diff --git a/CHANGES/12641.contrib.rst b/CHANGES/12641.contrib.rst deleted file mode 100644 index f431e0fd1e3..00000000000 --- a/CHANGES/12641.contrib.rst +++ /dev/null @@ -1,5 +0,0 @@ -Made the ``pip`` command used by the :file:`Makefile` configurable via a -``PIP`` variable; downstream consumers can now run, for example, -``make .develop PIP="uv pip"`` to install via ``uv`` without us -maintaining a parallel target --- by :user:`bdraco`. diff --git a/CHANGES/12643.contrib.rst b/CHANGES/12643.contrib.rst deleted file mode 100644 index 8477457ce16..00000000000 --- a/CHANGES/12643.contrib.rst +++ /dev/null @@ -1,5 +0,0 @@ -Merged the "Update pip, wheel, setuptools, build, twine" step into -the following ``Install dependencies`` (or ``Install cython``) step -in every job in :file:`.github/workflows/ci-cd.yml`, so each job now -runs a single ``pip``/``uv pip install`` invocation instead of two --- by :user:`bdraco`. diff --git a/CHANGES/12647.contrib.rst b/CHANGES/12647.contrib.rst deleted file mode 100644 index 5203ba0a395..00000000000 --- a/CHANGES/12647.contrib.rst +++ /dev/null @@ -1,4 +0,0 @@ -Override ``CIBW_BUILD_FRONTEND=build`` on the QEMU-emulated odd-arch wheel -jobs so cibuildwheel falls back to plain pip, because the pypa -``manylinux``/``musllinux`` containers for those arches do not ship ``uv`` -preinstalled -- by :user:`bdraco`. diff --git a/CHANGES/12651.contrib.rst b/CHANGES/12651.contrib.rst deleted file mode 100644 index 0318ed0d084..00000000000 --- a/CHANGES/12651.contrib.rst +++ /dev/null @@ -1,5 +0,0 @@ -Allowed re-running the ``deploy`` job in ``.github/workflows/ci-cd.yml`` -after a partial release failure: the ``Make Release`` step now skips -when the GitHub Release already exists, and the PyPI publish step uses -``skip-existing`` so dists that were already uploaded on a prior -attempt do not break the retry -- by :user:`bdraco`. diff --git a/CHANGES/12655.contrib.rst b/CHANGES/12655.contrib.rst deleted file mode 100644 index f5c76cd4105..00000000000 --- a/CHANGES/12655.contrib.rst +++ /dev/null @@ -1,4 +0,0 @@ -Switched the armv7l wheel builds onto GitHub's hosted ARM runners. The -32-bit ARM build still runs under QEMU, but the host is now aarch64 -rather than x86_64, so the emulation overhead drops sharply --- by :user:`bdraco`. diff --git a/CHANGES/12678.packaging.rst b/CHANGES/12678.packaging.rst deleted file mode 100644 index 7e459cf9ff5..00000000000 --- a/CHANGES/12678.packaging.rst +++ /dev/null @@ -1 +0,0 @@ -Submitted vendored `llhttp` to Github's SBOM -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12681.packaging.rst b/CHANGES/12681.packaging.rst deleted file mode 100644 index 1cd12837ecb..00000000000 --- a/CHANGES/12681.packaging.rst +++ /dev/null @@ -1 +0,0 @@ -Updated ``llhttp`` to v9.4.1 -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12689.breaking.rst b/CHANGES/12689.breaking.rst deleted file mode 100644 index 9d807e00769..00000000000 --- a/CHANGES/12689.breaking.rst +++ /dev/null @@ -1,7 +0,0 @@ -Tightened outbound header serialization to reject all ASCII control -characters forbidden by :rfc:`9110#section-5.5` and :rfc:`9112#section-4` -(``0x00``\-``0x08``, ``0x0A``\-``0x1F``, ``0x7F``) in status lines, -header field-names, and field-values. Previously only CR, LF and NUL were -rejected. HTAB (``0x09``) remains permitted in field values. Applications -that placed bare control characters in outbound headers will now raise -:exc:`ValueError` instead of emitting non-RFC-compliant bytes -- by :user:`rodrigobnogueira`. diff --git a/CHANGES/12703.bugfix.rst b/CHANGES/12703.bugfix.rst deleted file mode 100644 index 7cfca2415c3..00000000000 --- a/CHANGES/12703.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed irrelevant URLs pointing to the corresponding sections in the documentation of Gunicorn -- by :user:`serhiiur`. diff --git a/CHANGES/12706.bugfix.rst b/CHANGES/12706.bugfix.rst deleted file mode 100644 index 9248585f99f..00000000000 --- a/CHANGES/12706.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed invalid bytes being allowed in multipart/payload headers -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12722.bugfix.rst b/CHANGES/12722.bugfix.rst deleted file mode 100644 index af8b103f566..00000000000 --- a/CHANGES/12722.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed :py:meth:`~aiohttp.FormData.add_field` accepting invalid bytes in ``name`` and ``filename`` -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12726.doc.rst b/CHANGES/12726.doc.rst deleted file mode 100644 index 45e1212acd1..00000000000 --- a/CHANGES/12726.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Removed archived and deprecated repositories from third party list -- by :user:`Polandia94`. diff --git a/CHANGES/12727.bugfix.rst b/CHANGES/12727.bugfix.rst deleted file mode 100644 index d74b5eae2d3..00000000000 --- a/CHANGES/12727.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed websocket upgrade occurring when header contained a value like `notupgrade` -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12727.doc.rst b/CHANGES/12727.doc.rst deleted file mode 100644 index 61ffeb82274..00000000000 --- a/CHANGES/12727.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Added ``aiointercept`` to list of third-party libraries -- by :user:`Polandia94`. diff --git a/CHANGES/12753.misc.rst b/CHANGES/12753.misc.rst deleted file mode 100644 index 0b73044f46a..00000000000 --- a/CHANGES/12753.misc.rst +++ /dev/null @@ -1,2 +0,0 @@ -Improved ``ContentLengthError`` exception messages to include both expected and received byte counts. This enhancement provides better diagnostics when debugging response body size mismatches --- by :user:`bdraco` and :user:`Dreamsorcerer`. diff --git a/CHANGES/3951.feature.rst b/CHANGES/3951.feature.rst deleted file mode 100644 index ba127c20a83..00000000000 --- a/CHANGES/3951.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Added :py:attr:`~aiohttp.CookieJar.cookies` and :py:attr:`~aiohttp.CookieJar.host_only_cookies` read-only properties to :py:class:`~aiohttp.CookieJar` exposing the stored cookies with their full attributes -- by :user:`Br1an67`. diff --git a/CHANGES/7157.bugfix.rst b/CHANGES/7157.bugfix.rst deleted file mode 100644 index 60f06d8da9f..00000000000 --- a/CHANGES/7157.bugfix.rst +++ /dev/null @@ -1,6 +0,0 @@ -Fixed ``ZLibDecompressor`` silently dropping data past the first -member when decompressing concatenated gzip/deflate streams. Each subsequent -member is now handed to a fresh decompressor, matching the behaviour already -implemented for ZSTD multi-frame streams. - --- by :user:`Ashutosh-177` diff --git a/CHANGES/9308.breaking.rst b/CHANGES/9308.breaking.rst deleted file mode 100644 index afb3965ab8f..00000000000 --- a/CHANGES/9308.breaking.rst +++ /dev/null @@ -1,12 +0,0 @@ -Stopped calling :func:`socket.getfqdn` as the fallback for -:attr:`aiohttp.web.BaseRequest.host`. :func:`socket.getfqdn` -performs blocking reverse DNS resolution on the event loop -thread and can stall a worker for many seconds when the system -resolver is slow, and could be triggered remotely by an HTTP/1.0 -request that omits the ``Host`` header. The fallback when no -``Host`` header is present is now the local socket address the -request arrived on (transport ``sockname``), or an empty string -if no transport information is available. Code that relied on -the FQDN being returned must now read it from -:func:`socket.getfqdn` directly, off the event loop --- by :user:`bdraco`. diff --git a/CHANGES/9705.contrib.rst b/CHANGES/9705.contrib.rst deleted file mode 100644 index 5eaef0c4398..00000000000 --- a/CHANGES/9705.contrib.rst +++ /dev/null @@ -1,2 +0,0 @@ -Reduced the runtime of a client timeout regression test by shortening its artificial response delay. --- by :user:`nightcityblade`. diff --git a/aiohttp/__init__.py b/aiohttp/__init__.py index 4bbf8d308ed..b6afca45c60 100644 --- a/aiohttp/__init__.py +++ b/aiohttp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "3.14.0.dev0" +__version__ = "3.14.0" from typing import TYPE_CHECKING From a1fc2691f695517186a987dc9960a929a0efd978 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 08:13:25 +0000 Subject: [PATCH 179/191] Bump pip from 26.1.1 to 26.1.2 (#12764) Bumps [pip](https://github.com/pypa/pip) from 26.1.1 to 26.1.2.
Changelog

Sourced from pip's changelog.

26.1.2 (2026-05-31)

Bug Fixes

  • Reject console_scripts and gui_scripts entry points whose name would install a script outside the scripts directory. ([#14000](https://github.com/pypa/pip/issues/14000) <https://github.com/pypa/pip/issues/14000>_)
  • Fix installation incorrectly failing when the target path contains a doubled slash, such as with pip install --root //.... ([#14001](https://github.com/pypa/pip/issues/14001) <https://github.com/pypa/pip/issues/14001>_)
  • Send a consistent Accept-Encoding header to avoid a spurious Cache entry deserialization failed warning. ([#14012](https://github.com/pypa/pip/issues/14012) <https://github.com/pypa/pip/issues/14012>_)
Commits
  • 31d7d16 Bump for release
  • 79f348c Update AUTHORS.txt
  • 237a925 Merge pull request #14001 from notatallshaw/fix-is-within-directory
  • 34d0285 Merge pull request #14006 from laymonage/fix-requirements_from_scripts-space-...
  • 09d3e07 Merge pull request #14012 from notatallshaw/stable-accept-encoding
  • fa7854f Use is_within_directory for entry point check
  • d01b46c NEWS ENTRY
  • 7ff8bdd Fix is_within_directory for doubled-slash roots
  • 7ea3466 NEWS ENTRY
  • 85673ea Fix Accept-Encoding to gzip, deflate
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pip&package-manager=pip&previous-version=26.1.1&new-version=26.1.2)](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 | 10 ++++------ requirements/dev.txt | 10 ++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 89b95c66ca4..f0d3f1efc64 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -148,6 +148,8 @@ packaging==26.2 # wheel pathspec==1.1.1 # via mypy +pip==26.1.2 + # via pip-tools pip-tools==7.5.3 # via -r requirements/dev.in pkgconfig==1.6.0 @@ -229,6 +231,8 @@ requests==2.34.2 # sphinxcontrib-spelling rich==15.0.0 # via pytest-codspeed +setuptools==82.0.1 + # via pip-tools setuptools-git==1.2 # via -r requirements/test-common-base.in six==1.17.0 @@ -315,9 +319,3 @@ zlib-ng==1.0.0 # via # -r requirements/lint.in # -r requirements/test-common.in - -# The following packages are considered to be unsafe in a requirements file: -pip==26.1.1 - # via pip-tools -setuptools==82.0.1 - # via pip-tools diff --git a/requirements/dev.txt b/requirements/dev.txt index a1525e6247d..ab52c26ee9a 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -145,6 +145,8 @@ packaging==26.2 # wheel pathspec==1.1.1 # via mypy +pip==26.1.2 + # via pip-tools pip-tools==7.5.3 # via -r requirements/dev.in pkgconfig==1.6.0 @@ -222,6 +224,8 @@ requests==2.34.2 # via sphinx rich==15.0.0 # via pytest-codspeed +setuptools==82.0.1 + # via pip-tools setuptools-git==1.2 # via -r requirements/test-common-base.in six==1.17.0 @@ -305,9 +309,3 @@ zlib-ng==1.0.0 # via # -r requirements/lint.in # -r requirements/test-common.in - -# The following packages are considered to be unsafe in a requirements file: -pip==26.1.1 - # via pip-tools -setuptools==82.0.1 - # via pip-tools From 3848cc95fd6b7c10ca65eddc161a98ca2f413252 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 09:11:07 +0000 Subject: [PATCH 180/191] Bump pytest from 9.0.2 to 9.0.3 (#12766) Bumps [pytest](https://github.com/pytest-dev/pytest) from 9.0.2 to 9.0.3.
Release notes

Sourced from pytest's releases.

9.0.3

pytest 9.0.3 (2026-04-07)

Bug fixes

  • #12444: Fixed pytest.approx which now correctly takes into account ~collections.abc.Mapping keys order to compare them.

  • #13634: Blocking a conftest.py file using the -p no: option is now explicitly disallowed.

    Previously this resulted in an internal assertion failure during plugin loading.

    Pytest now raises a clear UsageError explaining that conftest files are not plugins and cannot be disabled via -p.

  • #13734: Fixed crash when a test raises an exceptiongroup with __tracebackhide__ = True.

  • #14195: Fixed an issue where non-string messages passed to unittest.TestCase.subTest() were not printed.

  • #14343: Fixed use of insecure temporary directory (CVE-2025-71176).

Improved documentation

  • #13388: Clarified documentation for -p vs PYTEST_PLUGINS plugin loading and fixed an incorrect -p example.
  • #13731: Clarified that capture fixtures (e.g. capsys and capfd) take precedence over the -s / --capture=no command-line options in Accessing captured output from a test function <accessing-captured-output>.
  • #14088: Clarified that the default pytest_collection hook sets session.items before it calls pytest_collection_finish, not after.
  • #14255: TOML integer log levels must be quoted: Updating reference documentation.

Contributor-facing changes

  • #12689: The test reports are now published to Codecov from GitHub Actions. The test statistics is visible on the web interface.

    -- by aleguy02

Commits

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/test-common-base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt index 9065c1c2340..cf525f96844 100644 --- a/requirements/test-common-base.txt +++ b/requirements/test-common-base.txt @@ -26,7 +26,7 @@ proxy-py==2.4.10 # via -r requirements/test-common-base.in pygments==2.19.2 # via pytest -pytest==9.0.2 +pytest==9.0.3 # via # -r requirements/test-common-base.in # pytest-cov From 2daf6a6db995cffec9fee935d321a3a083d753d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 09:32:51 +0000 Subject: [PATCH 181/191] Bump tomli from 2.4.0 to 2.4.1 (#12769) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [tomli](https://github.com/hukkin/tomli) from 2.4.0 to 2.4.1.
Changelog

Sourced from tomli's changelog.

2.4.1

  • Fixed
    • Limit number of parts of a TOML key to address quadratic time complexity
Commits

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/test-common-base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt index cf525f96844..f3bf694c5d2 100644 --- a/requirements/test-common-base.txt +++ b/requirements/test-common-base.txt @@ -44,7 +44,7 @@ setuptools-git==1.2 # via -r requirements/test-common-base.in six==1.17.0 # via python-dateutil -tomli==2.4.0 +tomli==2.4.1 # via # coverage # pytest From 7f4ed944cad5392c31f39cb355e253be9d696c13 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 09:50:21 +0000 Subject: [PATCH 182/191] Bump packaging from 26.0 to 26.2 (#12772) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [packaging](https://github.com/pypa/packaging) from 26.0 to 26.2.
Release notes

Sourced from packaging's releases.

26.2

What's Changed

Fixes:

Documentation:

Internal:

New Contributors

Full Changelog: https://github.com/pypa/packaging/compare/26.1...26.2

26.1

Features:

Behavior adaptations:

... (truncated)

Changelog

Sourced from packaging's changelog.

26.2 - 2026-04-24


Fixes:
  • Fix incorrect sysconfig var name for pyemscripten in (:pull:1160)
  • Make Version, Specifier, SpecifierSet, Tag, Marker, and Requirement pickle-safe
    and backward-compatible with pickles created in 25.0-26.1 (including references to the removed
    packaging._structures module) (:pull:1163, :pull:1168, :pull:1170, :pull:1171)
  • Re-export ExceptionGroup in metadata for now in (:pull:1164)

Documentation:

  • Add errors section and fix missing details in (:pull:1159)
  • Document our property-based test suite in (:pull:1167)
  • Fix a DirectUrl typo in (:pull:1169)
  • Add example of is_unsatisfiable in (:pull:1166)

Internal:

  • Enable the auditor persona on zizmor in (:pull:1158)
  • Test new pickle guarantees in (:pull:1174)
  • Use new native ReadTheDocs uv integration in (:pull:1175)

26.1 - 2026-04-14

Features:

  • PEP 783: add handling for Emscripten wheel tags in (:pull:804) (old name used in implementation, fixed in next release)
  • PEP 803: add handling for the abi3.abi3t free-threading tag in (:pull:1099)
  • PEP 723: add packaging.dependency_groups module, based on the dependency-groups package in (:pull:1065)
  • Add the packaging.direct_url module in (:pull:944)
  • Add the packaging.errors module in (:pull:1071)
  • Add SpecifierSet.is_unsatisfiable using ranges (new internals that will be expanded in future versions) in (:pull:1119)
  • Add create_compatible_tags_selector to select compatible tags in (:pull:1110)
  • Add a key argument to SpecifierSet.filter() in (:pull:1068)
  • Support & and | for Marker's in (:pull:1146)
  • Normalize Version.__replace__ and add Version.from_parts in (:pull:1078)
  • Add an option to validate compressed tag set sort order in parse_wheel_filename in (:pull:1150)

Behavior adaptations:

  • Narrow exclusion of pre-releases for <V.postN to match spec in (:pull:1140)
  • Narrow exclusion of post-releases for >V to match spec in (:pull:1141)
  • Rename format_full_version to _format_full_version to make it visibly private in (:pull:1125)
  • Restrict local version to ASCII in (:pull:1102)

Pylock (PEP 751) updates:

... (truncated)

Commits

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/test-common-base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt index f3bf694c5d2..acaa4aa9347 100644 --- a/requirements/test-common-base.txt +++ b/requirements/test-common-base.txt @@ -14,7 +14,7 @@ freezegun==1.5.5 # via -r requirements/test-common-base.in iniconfig==2.3.0 # via pytest -packaging==26.0 +packaging==26.2 # via pytest pkgconfig==1.6.0 # via -r requirements/test-common-base.in From 0cbc93e95289060cf5fa216c7706d7e3017b99ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 09:58:32 +0000 Subject: [PATCH 183/191] Bump pytest-cov from 7.0.0 to 7.1.0 (#12768) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 7.0.0 to 7.1.0.
Changelog

Sourced from pytest-cov's changelog.

7.1.0 (2026-03-21)

  • Fixed total coverage computation to always be consistent, regardless of reporting settings. Previously some reports could produce different total counts, and consequently can make --cov-fail-under behave different depending on reporting options. See [#641](https://github.com/pytest-dev/pytest-cov/issues/641) <https://github.com/pytest-dev/pytest-cov/issues/641>_.

  • Improve handling of ResourceWarning from sqlite3.

    The plugin adds warning filter for sqlite3 ResourceWarning unclosed database (since 6.2.0). It checks if there is already existing plugin for this message by comparing filter regular expression. When filter is specified on command line the message is escaped and does not match an expected message. A check for an escaped regular expression is added to handle this case.

    With this fix one can suppress ResourceWarning from sqlite3 from command line::

    pytest -W "ignore:unclosed database in <sqlite3.Connection object at:ResourceWarning" ...

  • Various improvements to documentation. Contributed by Art Pelling in [#718](https://github.com/pytest-dev/pytest-cov/issues/718) <https://github.com/pytest-dev/pytest-cov/pull/718>_ and "vivodi" in [#738](https://github.com/pytest-dev/pytest-cov/issues/738) <https://github.com/pytest-dev/pytest-cov/pull/738>. Also closed [#736](https://github.com/pytest-dev/pytest-cov/issues/736) <https://github.com/pytest-dev/pytest-cov/issues/736>.

  • Fixed some assertions in tests. Contributed by in Markéta Machová in [#722](https://github.com/pytest-dev/pytest-cov/issues/722) <https://github.com/pytest-dev/pytest-cov/pull/722>_.

  • Removed unnecessary coverage configuration copying (meant as a backup because reporting commands had configuration side-effects before coverage 5.0).

Commits
  • 66c8a52 Bump version: 7.0.0 → 7.1.0
  • f707662 Make the examples use pypy 3.11.
  • 6049a78 Make context test use the old ctracer (seems the new sysmon tracer behaves di...
  • 8ebf20b Update changelog.
  • 861d30e Remove the backup context manager - shouldn't be needed since coverage 5.0, ...
  • fd4c956 Pass the precision on the nulled total (seems that there's some caching goion...
  • 78c9c4e Only run the 3.9 on older deps.
  • 4849a92 Punctuation.
  • 197c35e Update changelog and hopefully I don't forget to publish release again :))
  • 14dc1c9 Update examples to use 3.11 and make the adhoc layout example look a bit more...
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/test-common-base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt index acaa4aa9347..fedf2066a58 100644 --- a/requirements/test-common-base.txt +++ b/requirements/test-common-base.txt @@ -32,7 +32,7 @@ pytest==9.0.3 # pytest-cov # pytest-mock # pytest-timeout -pytest-cov==7.0.0 +pytest-cov==7.1.0 # via -r requirements/test-common-base.in pytest-mock==3.15.1 # via -r requirements/test-common-base.in From c84f414acbb03f02ee2ece07a0c963b96ada3e81 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 09:58:48 +0000 Subject: [PATCH 184/191] Bump pygments from 2.19.2 to 2.20.0 (#12767) Bumps [pygments](https://github.com/pygments/pygments) from 2.19.2 to 2.20.0.
Release notes

Sourced from pygments's releases.

2.20.0

  • New lexers:

  • Updated lexers:

    • archetype: Fix catastrophic backtracking in GUID and ID patterns (#3064)
    • ASN.1: Recognize minus sign and fix range operator (#3014, #3060)
    • C++: Add C++26 keywords (#2955), add integer literal suffixes (#2966)
    • ComponentPascal: Fix analyse_text (#3028, #3032)
    • Coq renamed to Rocq (#2883, #2908)
    • Cython: Various improvements (#2932, #2933)
    • Debian control: Improve architecture parsing (#3052)
    • Devicetree: Add support for overlay/fragments (#3021), add bytestring support (#3022), fix catastrophic backtracking (#3057)
    • Fennel: Various improvements (#2911)
    • Haskell: Handle escape sequences in character literals (#3069, #1795)
    • Java: Add module keywords (#2955)
    • Lean4: Add operators ]', ]?, ]! (#2946)
    • LESS: Support single-line comments (#3005)
    • LilyPond: Update to 2.25.29 (#2974)
    • LLVM: Support C-style comments (#3023, #2978)
    • Lua(u): Fix catastrophic backtracking (#3047)
    • Macaulay2: Update to 1.25.05 (#2893), 1.25.11 (#2988)
    • Mathematica: Various improvements (#2957)
    • meson: Add additional operators (#2919)
    • MySQL: Update keywords (#2970)
    • org-Mode: Support both schedule and deadline (#2899)
    • PHP: Add __PROPERTY__ magic constant (#2924), add reserved keywords (#3002)
    • PostgreSQL: Add more keywords (#2985)
    • protobuf: Fix namespace tokenization (#2929)
    • Python: Add t-string support (#2973, #3009, #3010)
    • Tablegen: Fix infinite loop (#2972, #2940)
    • Tera Term macro: Add commands introduced in v5.3 through v5.6 (#2951)
    • TOML: Support TOML 1.1.0 (#3026, #3027)
    • Turtle: Allow empty comment lines (#2980)
    • XML: Added .xbrl as file ending (#2890, #2891)
  • Drop Python 3.8, and add Python 3.14 as a supported version (#2987, #3012)

  • Various improvements to autopygmentize (#2894)

  • Update onedark style to support more token types (#2977)

  • Update rtt style to support more token types (#2895)

  • Cache entry points to improve performance (#2979)

  • Fix xterm-256 color table (#3043)

  • Fix kwargs dictionary getting mutated on each call (#3044)

Changelog

Sourced from pygments's changelog.

Version 2.20.0

(released March 29th, 2026)

  • New lexers:

  • Updated lexers:

    • archetype: Fix catastrophic backtracking in GUID and ID patterns (#3064)
    • ASN.1: Recognize minus sign and fix range operator (#3014, #3060)
    • C++: Add C++26 keywords (#2955), add integer literal suffixes (#2966)
    • ComponentPascal: Fix analyse_text (#3028, #3032)
    • Coq renamed to Rocq (#2883, #2908)
    • Cython: Various improvements (#2932, #2933)
    • Debian control: Improve architecture parsing (#3052)
    • Devicetree: Add support for overlay/fragments (#3021), add bytestring support (#3022), fix catastrophic backtracking (#3057)
    • Fennel: Various improvements (#2911)
    • Haskell: Handle escape sequences in character literals (#3069, #1795)
    • Java: Add module keywords (#2955)
    • Lean4: Add operators ]', ]?, ]! (#2946)
    • LESS: Support single-line comments (#3005)
    • LilyPond: Update to 2.25.29 (#2974)
    • LLVM: Support C-style comments (#3023, #2978)
    • Lua(u): Fix catastrophic backtracking (#3047)
    • Macaulay2: Update to 1.25.05 (#2893), 1.25.11 (#2988)
    • Mathematica: Various improvements (#2957)
    • meson: Add additional operators (#2919)
    • MySQL: Update keywords (#2970)
    • org-Mode: Support both schedule and deadline (#2899)
    • PHP: Add __PROPERTY__ magic constant (#2924), add reserved keywords (#3002)
    • PostgreSQL: Add more keywords (#2985)
    • protobuf: Fix namespace tokenization (#2929)
    • Python: Add t-string support (#2973, #3009, #3010)
    • Tablegen: Fix infinite loop (#2972, #2940)
    • Tera Term macro: Add commands introduced in v5.3 through v5.6 (#2951)
    • TOML: Support TOML 1.1.0 (#3026, #3027)
    • Turtle: Allow empty comment lines (#2980)
    • XML: Added .xbrl as file ending (#2890, #2891)
  • Drop Python 3.8, and add Python 3.14 as a supported version (#2987, #3012)

  • Various improvements to autopygmentize (#2894)

  • Update onedark style to support more token types (#2977)

  • Update rtt style to support more token types (#2895)

  • Cache entry points to improve performance (#2979)

  • Fix xterm-256 color table (#3043)

  • Fix kwargs dictionary getting mutated on each call (#3044)

Commits
  • 708197d Fix underline length.
  • 1d4538a Prepare 2.20 release.
  • 2ceaee4 Update CHANGES.
  • e3a3c54 Fix Haskell lexer: handle escape sequences in character literals (#3069)
  • d7c3453 Merge pull request #3071 from pygments/harden-html-formatter
  • 0f97e7c Harden the HTML formatter against CSS.
  • 9f981b2 Update CHANGES.
  • 1d88915 Update CHANGES.
  • c3d93ad Fix ASN.1 lexer: recognize minus sign and fix range operator (#3060)
  • 4f06bcf fix bad behaving backtracking regex in CommonLispLexer
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/test-common-base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt index fedf2066a58..fc94dc894e0 100644 --- a/requirements/test-common-base.txt +++ b/requirements/test-common-base.txt @@ -24,7 +24,7 @@ pluggy==1.6.0 # pytest-cov proxy-py==2.4.10 # via -r requirements/test-common-base.in -pygments==2.19.2 +pygments==2.20.0 # via pytest pytest==9.0.3 # via From 635266b287b59b38b0e9009e0780a9667ebea599 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 2 Jun 2026 15:11:23 +0100 Subject: [PATCH 185/191] Fix CHANGES.rst (#12775) --- CHANGES.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index f876266d68c..97689f035a0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -32,7 +32,7 @@ Features - Added :func:`~aiohttp.encode_basic_auth` for encoding HTTP Basic Authentication credentials. Replaces the now-deprecated - :class:`~aiohttp.BasicAuth` -- by :user:`Dreamsorcerer`. + ``aiohttp.BasicAuth`` -- by :user:`Dreamsorcerer`. *Related issues and pull requests on GitHub:* @@ -300,7 +300,7 @@ Bug fixes Deprecations (removal in next major release) -------------------------------------------- -- Deprecated :class:`~aiohttp.BasicAuth` and the ``auth`` / ``proxy_auth`` +- Deprecated ``aiohttp.BasicAuth`` and the ``auth`` / ``proxy_auth`` parameters. They will be removed in aiohttp 4.0. Use the new :func:`~aiohttp.encode_basic_auth` helper together with ``headers={"Authorization": ...}`` (or From c66e5cb4702b7e3a7373efe2848e35a61fe95e6e Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 2 Jun 2026 15:44:13 +0100 Subject: [PATCH 186/191] Point Dependabot to 3.15 branch (#12774) --- .github/dependabot.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6ac64362454..fddd531da59 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -25,7 +25,7 @@ updates: directory: "/" labels: - dependencies - target-branch: "3.14" + target-branch: "3.15" schedule: interval: "daily" open-pull-requests-limit: 10 @@ -37,7 +37,7 @@ updates: - dependency-type: "all" labels: - dependencies - target-branch: "3.14" + target-branch: "3.15" schedule: interval: "daily" open-pull-requests-limit: 10 @@ -53,6 +53,6 @@ updates: directory: "/tests/autobahn/" labels: - dependencies - target-branch: "3.14" + target-branch: "3.15" schedule: interval: "monthly" From a3c548728f6b62c4e699a590cc3dc8a705a4e203 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:49:45 +0000 Subject: [PATCH 187/191] Bump idna from 3.17 to 3.18 (#12778) Bumps [idna](https://github.com/kjd/idna) from 3.17 to 3.18.
Changelog

Sourced from idna's changelog.

3.18 (2026-06-02)

  • When decoding a domain, add a display argument that will pass through invalid labels rather than raising an exception.
Commits
  • f39ea90 Release 3.18
  • 40f4e40 Pre-release 3.18rc0
  • 1a5bf80 Merge pull request #253 from kjd/lenient-decode
  • 5bbb26f Merge branch 'master' into lenient-decode
  • c532bae Rename decode() lenient= option to display= (issue #248)
  • 0b1758b Merge pull request #252 from kjd/release-3.17
  • 47b5cde Add lenient option to decode() for best-effort label recovery (issue #248)
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=idna&package-manager=pip&previous-version=3.17&new-version=3.18)](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/base-ft.txt | 2 +- requirements/base.txt | 2 +- requirements/constraints.txt | 2 +- requirements/dev.txt | 2 +- requirements/doc-spelling.txt | 2 +- requirements/doc.txt | 2 +- requirements/lint.txt | 2 +- requirements/runtime-deps.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 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt index 1eeed867279..eddfe0ba674 100644 --- a/requirements/base-ft.txt +++ b/requirements/base-ft.txt @@ -24,7 +24,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==26.0.0 # via -r requirements/base-ft.in -idna==3.17 +idna==3.18 # via yarl multidict==6.7.1 # via diff --git a/requirements/base.txt b/requirements/base.txt index 159c65a0ce2..863268a60c8 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -24,7 +24,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==26.0.0 # via -r requirements/base.in -idna==3.17 +idna==3.18 # via yarl multidict==6.7.1 # via diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 01204e1ff22..2d0f6d47207 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -100,7 +100,7 @@ gunicorn==26.0.0 # via -r requirements/base.in identify==2.6.19 # via pre-commit -idna==3.17 +idna==3.18 # via # requests # trustme diff --git a/requirements/dev.txt b/requirements/dev.txt index bb5cab60d3f..afa68f03c45 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -98,7 +98,7 @@ gunicorn==26.0.0 # via -r requirements/base.in identify==2.6.19 # via pre-commit -idna==3.17 +idna==3.18 # via # requests # trustme diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index 2878edd94f6..e5c3306697f 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -20,7 +20,7 @@ docutils==0.21.2 # via # myst-parser # sphinx -idna==3.17 +idna==3.18 # via requests imagesize==2.0.0 # via sphinx diff --git a/requirements/doc.txt b/requirements/doc.txt index 55d30f206cb..1a68ba65c49 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -20,7 +20,7 @@ docutils==0.21.2 # via # myst-parser # sphinx -idna==3.17 +idna==3.18 # via requests imagesize==2.0.0 # via sphinx diff --git a/requirements/lint.txt b/requirements/lint.txt index 198f532bfa7..7d88216875f 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -54,7 +54,7 @@ frozenlist==1.8.0 # aiosignal identify==2.6.19 # via pre-commit -idna==3.17 +idna==3.18 # via # trustme # yarl diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index f7080a0e341..859820baf29 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -22,7 +22,7 @@ frozenlist==1.8.0 # via # -r requirements/runtime-deps.in # aiosignal -idna==3.17 +idna==3.18 # via yarl multidict==6.7.1 # via diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt index ae3ed0da85e..c10df074bf5 100644 --- a/requirements/test-common-base.txt +++ b/requirements/test-common-base.txt @@ -26,7 +26,7 @@ frozenlist==1.8.0 # via # aiohttp # aiosignal -idna==3.17 +idna==3.18 # via yarl iniconfig==2.3.0 # via pytest diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 205bf161ec0..cd305172d11 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -42,7 +42,7 @@ frozenlist==1.8.0 # via # aiohttp # aiosignal -idna==3.17 +idna==3.18 # via # trustme # yarl diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 97aff3e0cd5..77c73738dee 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -59,7 +59,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==26.0.0 # via -r requirements/base-ft.in -idna==3.17 +idna==3.18 # via # trustme # yarl diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt index 2cfbd3ec357..459d80d34ac 100644 --- a/requirements/test-mobile.txt +++ b/requirements/test-mobile.txt @@ -47,7 +47,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==26.0.0 # via -r requirements/base-ft.in -idna==3.17 +idna==3.18 # via yarl iniconfig==2.3.0 # via pytest diff --git a/requirements/test.txt b/requirements/test.txt index 3f524555daa..66deab1b0e6 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -59,7 +59,7 @@ frozenlist==1.8.0 # aiosignal gunicorn==26.0.0 # via -r requirements/base.in -idna==3.17 +idna==3.18 # via # trustme # yarl From ec8591eee72dab4374c7a6a892d084495f3012b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:10:20 +0000 Subject: [PATCH 188/191] Bump pytest-cov from 7.0.0 to 7.1.0 (#12781) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 7.0.0 to 7.1.0.
Changelog

Sourced from pytest-cov's changelog.

7.1.0 (2026-03-21)

  • Fixed total coverage computation to always be consistent, regardless of reporting settings. Previously some reports could produce different total counts, and consequently can make --cov-fail-under behave different depending on reporting options. See [#641](https://github.com/pytest-dev/pytest-cov/issues/641) <https://github.com/pytest-dev/pytest-cov/issues/641>_.

  • Improve handling of ResourceWarning from sqlite3.

    The plugin adds warning filter for sqlite3 ResourceWarning unclosed database (since 6.2.0). It checks if there is already existing plugin for this message by comparing filter regular expression. When filter is specified on command line the message is escaped and does not match an expected message. A check for an escaped regular expression is added to handle this case.

    With this fix one can suppress ResourceWarning from sqlite3 from command line::

    pytest -W "ignore:unclosed database in <sqlite3.Connection object at:ResourceWarning" ...

  • Various improvements to documentation. Contributed by Art Pelling in [#718](https://github.com/pytest-dev/pytest-cov/issues/718) <https://github.com/pytest-dev/pytest-cov/pull/718>_ and "vivodi" in [#738](https://github.com/pytest-dev/pytest-cov/issues/738) <https://github.com/pytest-dev/pytest-cov/pull/738>. Also closed [#736](https://github.com/pytest-dev/pytest-cov/issues/736) <https://github.com/pytest-dev/pytest-cov/issues/736>.

  • Fixed some assertions in tests. Contributed by in Markéta Machová in [#722](https://github.com/pytest-dev/pytest-cov/issues/722) <https://github.com/pytest-dev/pytest-cov/pull/722>_.

  • Removed unnecessary coverage configuration copying (meant as a backup because reporting commands had configuration side-effects before coverage 5.0).

Commits
  • 66c8a52 Bump version: 7.0.0 → 7.1.0
  • f707662 Make the examples use pypy 3.11.
  • 6049a78 Make context test use the old ctracer (seems the new sysmon tracer behaves di...
  • 8ebf20b Update changelog.
  • 861d30e Remove the backup context manager - shouldn't be needed since coverage 5.0, ...
  • fd4c956 Pass the precision on the nulled total (seems that there's some caching goion...
  • 78c9c4e Only run the 3.9 on older deps.
  • 4849a92 Punctuation.
  • 197c35e Update changelog and hopefully I don't forget to publish release again :))
  • 14dc1c9 Update examples to use 3.11 and make the adhoc layout example look a bit more...
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pytest-cov&package-manager=pip&previous-version=7.0.0&new-version=7.1.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 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/test-common-base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt index c10df074bf5..86acaec4339 100644 --- a/requirements/test-common-base.txt +++ b/requirements/test-common-base.txt @@ -62,7 +62,7 @@ pytest-aiohttp==1.1.0 # via -r requirements/test-common-base.in pytest-asyncio==1.3.0 # via pytest-aiohttp -pytest-cov==7.0.0 +pytest-cov==7.1.0 # via -r requirements/test-common-base.in pytest-mock==3.15.1 # via -r requirements/test-common-base.in From f8efc27f2fa2df66e50e55ef329638a9b5ffb794 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:11:50 +0000 Subject: [PATCH 189/191] Bump pytest-asyncio from 1.3.0 to 1.4.0 (#12783) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [//]: # (dependabot-start) ⚠️ **Dependabot is rebasing this PR** ⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 1.3.0 to 1.4.0.
Release notes

Sourced from pytest-asyncio's releases.

pytest-asyncio v1.4.0

1.4.0 - 2026-05-26

Deprecated

  • Overriding the event_loop_policy fixture is deprecated. Use the pytest_asyncio_loop_factories hook instead. (#1419)

Added

  • Added the pytest_asyncio_loop_factories hook to parametrize asyncio tests with custom event loop factories.

    The hook returns a mapping of factory names to loop factories, and pytest.mark.asyncio(loop_factories=[...]) selects a subset of configured factories per test. When a single factory is configured, test names are unchanged.

    Synchronous @pytest_asyncio.fixture functions now see the correct event loop when custom loop factories are configured, even when test code disrupts the current event loop (e.g., via asyncio.run() or asyncio.set_event_loop(None)). (#1164)

Changed

  • Improved the readability of the warning message that is displayed when asyncio_default_fixture_loop_scope is unset (#1298)
  • Only import asyncio.AbstractEventLoopPolicy for type checking to avoid raising a DeprecationWarning. (#1394)
  • Updated minimum supported pytest version to v8.4.0. (#1397)

Fixed

  • Fixed a ResourceWarning: unclosed event loop warning that could occur when a synchronous test called asyncio.run() or otherwise unset the current event loop after pytest-asyncio had run an async test or fixture. (#724)

Notes for Downstream Packagers

  • Added dependency on sphinx-tabs >= 3.5 to organize documentation examples into tabs. (#1395)

pytest-asyncio v1.4.0a2

1.4.0a2 - 2026-05-02

Deprecated

  • Overriding the event_loop_policy fixture is deprecated. Use the pytest_asyncio_loop_factories hook instead. (#1419)

Added

  • Added the pytest_asyncio_loop_factories hook to parametrize asyncio tests with custom event loop factories.

    The hook returns a mapping of factory names to loop factories, and pytest.mark.asyncio(loop_factories=[...]) selects a subset of configured factories per test. When a single factory is configured, test names are unchanged on pytest 8.4+.

    Synchronous @pytest_asyncio.fixture functions now see the correct event loop when custom loop factories are configured, even when test code disrupts the current event loop (e.g., via asyncio.run() or asyncio.set_event_loop(None)). (#1164)

Changed

  • Improved the readability of the warning message that is displayed when asyncio_default_fixture_loop_scope is unset (#1298)
  • Only import asyncio.AbstractEventLoopPolicy for type checking to avoid raising a DeprecationWarning. (#1394)

... (truncated)

Commits
  • 6e14cd2 chore: Prepare release of v1.4.0.
  • 4b900fb Build(deps): Bump codecov/codecov-action from 6.0.0 to 6.0.1
  • ab9f632 Build(deps): Bump zipp from 3.23.1 to 4.1.0
  • a56fc77 Build(deps): Bump hypothesis from 6.152.6 to 6.152.8
  • e8bae9b Build(deps): Bump requests from 2.34.0 to 2.34.2
  • fc43340 Build(deps): Bump idna from 3.14 to 3.15
  • 762eaf5 Build(deps): Bump jaraco-functools from 4.4.0 to 4.5.0
  • b62e222 Build(deps): Bump click from 8.3.3 to 8.4.0
  • 9190447 Build(deps): Bump pydantic from 2.13.3 to 2.13.4
  • 82a393c ci: Remove unnecessary debug output.
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pytest-asyncio&package-manager=pip&previous-version=1.3.0&new-version=1.4.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 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/test-common-base.txt | 2 +- requirements/test-mobile.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt index 86acaec4339..a9645132192 100644 --- a/requirements/test-common-base.txt +++ b/requirements/test-common-base.txt @@ -60,7 +60,7 @@ pytest==9.0.3 # pytest-timeout pytest-aiohttp==1.1.0 # via -r requirements/test-common-base.in -pytest-asyncio==1.3.0 +pytest-asyncio==1.4.0 # via pytest-aiohttp pytest-cov==7.1.0 # via -r requirements/test-common-base.in diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt index 459d80d34ac..7e874b4c008 100644 --- a/requirements/test-mobile.txt +++ b/requirements/test-mobile.txt @@ -91,7 +91,7 @@ pytest==9.0.3 # pytest-timeout pytest-aiohttp==1.1.0 # via -r requirements/test-common-base.in -pytest-asyncio==1.4.0a2 +pytest-asyncio==1.4.0 # via pytest-aiohttp pytest-cov==7.1.0 # via -r requirements/test-common-base.in From 3fae5596c620d4a2b29847d65691387097bdeacd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:13:42 +0000 Subject: [PATCH 190/191] Bump distlib from 0.4.0 to 0.4.1 (#12785) Bumps [distlib](https://github.com/pypa/distlib) from 0.4.0 to 0.4.1.
Changelog

Sourced from distlib's changelog.

0.4.1


Released: 2026-06-02
  • scripts

    • Fix path traversal bug in handling entry points which allowed escaping the scripts directory. Thanks to tonghuaroot for the comprehensive report.
  • tests

    • Fix #251: Change test function following a reorganization which happened in the Python stdlib.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=distlib&package-manager=pip&previous-version=0.4.0&new-version=0.4.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 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 2d0f6d47207..0e1600af659 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -71,7 +71,7 @@ cryptography==48.0.0 # via trustme cython==3.2.5 # via -r requirements/cython.in -distlib==0.4.0 +distlib==0.4.1 # via virtualenv docutils==0.21.2 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index afa68f03c45..8a34e18868e 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -69,7 +69,7 @@ coverage==7.14.1 # pytest-cov cryptography==48.0.0 # via trustme -distlib==0.4.0 +distlib==0.4.1 # via virtualenv docutils==0.21.2 # via diff --git a/requirements/lint.txt b/requirements/lint.txt index 7d88216875f..b76fb26d65f 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -36,7 +36,7 @@ click==8.4.1 # via slotscheck cryptography==48.0.0 # via trustme -distlib==0.4.0 +distlib==0.4.1 # via virtualenv exceptiongroup==1.3.1 # via pytest From f387f620459cf5b8e0e1df2f18dc70e9c3d29909 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:14:38 +0000 Subject: [PATCH 191/191] Bump click from 8.3.1 to 8.4.1 (#12784) Bumps [click](https://github.com/pallets/click) from 8.3.1 to 8.4.1.
Release notes

Sourced from click's releases.

8.4.1

This is the Click 8.4.1 fix release, which fixes bugs but does not otherwise change behavior and should not result in breaking changes compared to the latest feature release.

PyPI: https://pypi.org/project/click/8.4.1/ Changes: https://click.palletsprojects.com/page/changes/#version-8-4-1 Milestone: https://github.com/pallets/click/milestone/32?closed=1

  • get_parameter_source() is available during eager callbacks and type conversion again. #3458 #3484
  • Zsh completion scripts parse correctly on Windows. #3277 # 3466
  • Shell completion of Choice Enum values produces a valid completion result. #3015
  • Fix empty byte-string handling in echo. #3487
  • Fix closed file error with echo_via_pager. #3449

8.4.0

This is the Click 8.4.0 feature release. A feature release may include new features, remove previously deprecated code, add new deprecation, or introduce potentially breaking changes.

We encourage everyone to upgrade. You can read more about our Version Support Policy on our website.

PyPI: https://pypi.org/project/click/8.4.0/ Changes: https://click.palletsprojects.com/page/changes/#version-8-4-0 Milestone https://github.com/pallets/click/milestone/30

  • ParamType typing improvements. #3371

    • :class:ParamType is now a generic abstract base class, parameterized by its converted value type.
    • :meth:~ParamType.convert return types are narrowed on all concrete types (str for :class:STRING, int for :class:INT, etc.).
    • :meth:~ParamType.to_info_dict returns specific :class:~typing.TypedDict subclasses instead of dict[str, Any].
    • :class:CompositeParamType and the number-range base are now generic with abstract methods.
  • Refactor convert_type to extract type inference into a private _guess_type helper, and add :func:typing.overload signatures. #3372

  • Parameter typing improvements. #2805

    • :class:Parameter is now an abstract base class, making explicit that it cannot be instantiated directly.
    • :attr:Parameter.name is now str instead of str | None. When expose_value=False, the name is set to "" instead of None.
    • The ctx parameter of :meth:Parameter.get_error_hint is now typed as Context | None, matching the runtime behavior.
  • Split string values from default_map for parameters with nargs > 1 or :class:Tuple type, matching environment variable behavior.

... (truncated)

Changelog

Sourced from click's changelog.

Version 8.4.1

Released 2026-05-21

  • get_parameter_source() is available during eager callbacks and type conversion again. :issue:3458 :issue:3484
  • Zsh completion scripts parse correctly on Windows. :issue:3277 :pr:3466
  • Shell completion of Choice Enum values produces a valid completion result. :issue:3015
  • Fix empty byte-string handling in echo. :issue:3487
  • Fix closed file error with echo_via_pager. :issue:3449

Version 8.4.0

Released 2026-05-17

  • :class:ParamType typing improvements. :pr:3371

    • :class:ParamType is now a generic abstract base class, parameterized by its converted value type.
    • :meth:~ParamType.convert return types are narrowed on all concrete types (str for :class:STRING, int for :class:INT, etc.).
    • :meth:~ParamType.to_info_dict returns specific :class:~typing.TypedDict subclasses instead of dict[str, Any].
    • :class:CompositeParamType and the number-range base are now generic with abstract methods.
  • Refactor convert_type to extract type inference into a private _guess_type helper, and add :func:typing.overload signatures. :pr:3372

  • :class:Parameter typing improvements. :pr:2805

    • :class:Parameter is now an abstract base class, making explicit that it cannot be instantiated directly.
    • :attr:Parameter.name is now str instead of str | None. When expose_value=False, the name is set to "" instead of None.
    • The ctx parameter of :meth:Parameter.get_error_hint is now typed as Context | None, matching the runtime behavior.
  • Split string values from default_map for parameters with nargs > 1 or :class:Tuple type, matching environment variable behavior. :issue:2745 :pr:3364

  • Auto-detect type=UNPROCESSED for flag_value of non-basic types (not str, int, float, or bool), so programmer-provided Python objects like classes and enum members are passed through unchanged instead of being stringified. Previously type=click.UNPROCESSED had to be set explicitly. :issue:2012 :pr:3363

... (truncated)

Commits
  • 6eeb50e release version 8.4.1
  • 67921d5 change log and doc fixes (#3495)
  • 9c41f46 Fix changelog and version admonitions
  • 6cb3477 fix skip condition
  • 5ee8e31 fix I/O operation on closed file error with CliRunner and echo_via_pager (#3482)
  • becbde5 pager doesn't close std streams
  • a5f5aa6 Handle empty bytes in echo (#3493)
  • 4d3db84 handle empty bytes in echo
  • d42f15b Fix get_parameter_source() during type conversion and eager callbacks (#3484)
  • 0baa8db Document ctx.params bypass with test and doc
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=click&package-manager=pip&previous-version=8.3.1&new-version=8.4.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 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/test-common-base.txt | 2 +- requirements/test-mobile.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt index a9645132192..8624b473f95 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 -click==8.3.1 +click==8.4.1 # via wait-for-it coverage==7.14.1 # via pytest-cov diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt index 7e874b4c008..f5c66fce657 100644 --- a/requirements/test-mobile.txt +++ b/requirements/test-mobile.txt @@ -32,7 +32,7 @@ cffi==2.0.0 ; sys_platform != "android" and sys_platform != "ios" # via # -r requirements/test-mobile.in # pycares -click==8.4.0 +click==8.4.1 # via wait-for-it coverage==7.14.1 # via pytest-cov