From 35c979be79dcc75ecbf7270ccc9fde264e5e2948 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Apr 2025 10:47:11 -1000 Subject: [PATCH 1/4] Fix headers being mutated if passed to web.Response as a CIMultiDict (#10672) ## What do these changes do? If `CIMultiDict` is passed in we need to make a copy to avoid mutating it. In some cases we used to copy these twice which was fixed in #10045 but for this case that was the only copy being made and the source of this regression. fixes #10670 --- CHANGES/10672.bugfix.rst | 1 + aiohttp/web_response.py | 4 +--- tests/test_web_response.py | 14 +++++++++++++- 3 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 CHANGES/10672.bugfix.rst diff --git a/CHANGES/10672.bugfix.rst b/CHANGES/10672.bugfix.rst new file mode 100644 index 00000000000..a4434f8c87a --- /dev/null +++ b/CHANGES/10672.bugfix.rst @@ -0,0 +1 @@ +Fixed :class:`multidict.CIMultiDict` being mutated when passed to :class:`aiohttp.web.Response` -- by :user:`bdraco`. diff --git a/aiohttp/web_response.py b/aiohttp/web_response.py index d1bb401a5e6..56596905a35 100644 --- a/aiohttp/web_response.py +++ b/aiohttp/web_response.py @@ -532,10 +532,8 @@ def __init__( if headers is None: real_headers: CIMultiDict[str] = CIMultiDict() - elif not isinstance(headers, CIMultiDict): - real_headers = CIMultiDict(headers) else: - real_headers = headers # = cast('CIMultiDict[str]', headers) + real_headers = CIMultiDict(headers) if content_type is not None and "charset" in content_type: raise ValueError("charset must not be in content_type argument") diff --git a/tests/test_web_response.py b/tests/test_web_response.py index 493e5e66f27..e5dd4dab7fb 100644 --- a/tests/test_web_response.py +++ b/tests/test_web_response.py @@ -12,7 +12,7 @@ import aiosignal import pytest -from multidict import CIMultiDict, CIMultiDictProxy +from multidict import CIMultiDict, CIMultiDictProxy, MultiDict from aiohttp import HttpVersion, HttpVersion10, HttpVersion11, hdrs, web from aiohttp.abc import AbstractStreamWriter @@ -1384,3 +1384,15 @@ async def test_warn_large_cookie(buf: bytearray, writer: AbstractStreamWriter) - assert match is not None cookie = match.group(1) assert len(cookie) == 4097 + + +@pytest.mark.parametrize("loose_header_type", (MultiDict, CIMultiDict, dict)) +async def test_passing_cimultidict_to_web_response_not_mutated( + loose_header_type: type, +) -> None: + req = make_request("GET", "/") + headers = loose_header_type({}) + resp = web.Response(body=b"answer", headers=headers) + await resp.prepare(req) + assert resp.content_length == 6 + assert not headers From 048ef4c617130ef5a29a70ad67e34d9395891ac1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Apr 2025 11:35:26 -1000 Subject: [PATCH 2/4] Remove useless nonlocal statements in tests (#10675) discovered by new flake8 in https://github.com/aio-libs/aiohttp/pull/10653 --- tests/test_client_ws_functional.py | 3 +-- tests/test_helpers.py | 1 - tests/test_web_server.py | 3 +-- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/test_client_ws_functional.py b/tests/test_client_ws_functional.py index 190096bbf5e..5ec6d251388 100644 --- a/tests/test_client_ws_functional.py +++ b/tests/test_client_ws_functional.py @@ -363,7 +363,6 @@ async def test_concurrent_close(aiohttp_client: AiohttpClient) -> None: client_ws: Optional[aiohttp.ClientWebSocketResponse] = None async def handler(request: web.Request) -> web.WebSocketResponse: - nonlocal client_ws ws = web.WebSocketResponse() await ws.prepare(request) @@ -960,7 +959,7 @@ async def delayed_send_frame( message: bytes, opcode: int, compress: Optional[int] = None ) -> None: assert opcode == WSMsgType.PING - nonlocal cancelled, ping_started + nonlocal cancelled ping_started.set_result(None) try: await asyncio.sleep(1) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 081760c92fa..8adc33f53fc 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -378,7 +378,6 @@ async def test_timer_context_timeout_does_swallow_cancellation() -> None: ctx = helpers.TimerContext(loop) async def task_with_timeout() -> None: - nonlocal ctx new_task = asyncio.current_task() assert new_task is not None with pytest.raises(asyncio.TimeoutError): diff --git a/tests/test_web_server.py b/tests/test_web_server.py index cf287743ba6..d4a678468ea 100644 --- a/tests/test_web_server.py +++ b/tests/test_web_server.py @@ -384,7 +384,6 @@ async def test_handler_cancellation(unused_port_socket: socket.socket) -> None: port = sock.getsockname()[1] async def on_request(request: web.Request) -> web.Response: - nonlocal event try: await asyncio.sleep(10) except asyncio.CancelledError: @@ -427,7 +426,7 @@ async def test_no_handler_cancellation(unused_port_socket: socket.socket) -> Non started = False async def on_request(request: web.Request) -> web.Response: - nonlocal done_event, started, timeout_event + nonlocal started started = True await asyncio.wait_for(timeout_event.wait(), timeout=5) done_event.set() From 832b719d472b6445239e6b586aa1f4625cfbd904 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 1 Apr 2025 22:14:28 +0000 Subject: [PATCH 3/4] [pre-commit.ci] pre-commit autoupdate (#10653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 7.1.2 → 7.2.0](https://github.com/PyCQA/flake8/compare/7.1.2...7.2.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 597d22ce69b..c51e384bbd2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -102,7 +102,7 @@ repos: - id: pyupgrade args: ['--py37-plus'] - repo: https://github.com/PyCQA/flake8 - rev: '7.1.2' + rev: '7.2.0' hooks: - id: flake8 additional_dependencies: From 1f7aa7db24f032726d3a870ab3a017bdcccb075c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Apr 2025 22:53:21 +0000 Subject: [PATCH 4/4] Bump multidict from 6.2.0 to 6.3.0 (#10668) Bumps [multidict](https://github.com/aio-libs/multidict) from 6.2.0 to 6.3.0.
Release notes

Sourced from multidict's releases.

6.3.0

Bug fixes

  • Set operations for KeysView and ItemsView of case-insensitive multidicts and their proxies are processed in case-insensitive manner.

    Related issues and pull requests on GitHub: #965.

  • Rewrote :class:multidict.CIMultiDict and it proxy to always return :class:multidict.istr keys. istr is derived from :class:str, thus the change is backward compatible.

    The performance boost is about 15% for some operations for C Extension, pure Python implementation have got a visible (15% - 230%) speedup as well.

    Related issues and pull requests on GitHub: #1097.

  • Fixed a crash when extending a multidict from multidict proxy if C Extensions were used.

    Related issues and pull requests on GitHub: #1100.

Features

  • Implemented a custom parser for METH_FASTCALL | METH_KEYWORDS protocol -- by :user:asvetlov.

    The patch re-enables fast call protocol in the :py:mod:multidict C Extension.

    Speedup is about 25%-30% for the library benchmarks for Python 3.12+.

    Related issues and pull requests on GitHub: #1070.

  • The C-extension no longer pre-allocates a Python exception object in lookup-related methods of :py:class:~multidict.MultiDict when the passed-in key is not found but default value is provided.

    Namely, this affects :py:meth:MultiDict.getone() <multidict.MultiDict.getone>, :py:meth:MultiDict.getall() <multidict.MultiDict.getall>, :py:meth:MultiDict.get() <multidict.MultiDict.get>, :py:meth:MultiDict.pop() <multidict.MultiDict.pop>, :py:meth:MultiDict.popone() <multidict.MultiDict.popone>, and :py:meth:MultiDict.popall() <multidict.MultiDict.popall>.

... (truncated)

Changelog

Sourced from multidict's changelog.

6.3.0

(2025-03-31)

Bug fixes

  • Set operations for KeysView and ItemsView of case-insensitive multidicts and their proxies are processed in case-insensitive manner.

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

  • Rewrote :class:multidict.CIMultiDict and it proxy to always return :class:multidict.istr keys. istr is derived from :class:str, thus the change is backward compatible.

    The performance boost is about 15% for some operations for C Extension, pure Python implementation have got a visible (15% - 230%) speedup as well.

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

  • Fixed a crash when extending a multidict from multidict proxy if C Extensions were used.

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

Features

  • Implemented a custom parser for METH_FASTCALL | METH_KEYWORDS protocol -- by :user:asvetlov.

    The patch re-enables fast call protocol in the :py:mod:multidict C Extension.

    Speedup is about 25%-30% for the library benchmarks for Python 3.12+.

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

  • The C-extension no longer pre-allocates a Python exception object in lookup-related methods of :py:class:~multidict.MultiDict when the passed-in key is not found but default value is provided.

    Namely, this affects :py:meth:MultiDict.getone() <multidict.MultiDict.getone>, :py:meth:MultiDict.getall() <multidict.MultiDict.getall>, :py:meth:`MultiDict.get()

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=multidict&package-manager=pip&previous-version=6.2.0&new-version=6.3.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/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.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 04cc47cb71e..df1453a8d0a 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -24,7 +24,7 @@ gunicorn==23.0.0 # via -r requirements/base.in idna==3.6 # via yarl -multidict==6.2.0 +multidict==6.3.1 # via # -r requirements/runtime-deps.in # yarl diff --git a/requirements/constraints.txt b/requirements/constraints.txt index bcd69547302..1510bf28d66 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -109,7 +109,7 @@ markupsafe==3.0.2 # via jinja2 mdurl==0.1.2 # via markdown-it-py -multidict==6.2.0 +multidict==6.3.1 # via # -r requirements/multidict.in # -r requirements/runtime-deps.in diff --git a/requirements/cython.txt b/requirements/cython.txt index fc290ab6688..5f2bbcb7c1f 100644 --- a/requirements/cython.txt +++ b/requirements/cython.txt @@ -6,7 +6,7 @@ # cython==3.0.12 # via -r requirements/cython.in -multidict==6.2.0 +multidict==6.3.1 # via -r requirements/multidict.in typing-extensions==4.12.2 # via multidict diff --git a/requirements/dev.txt b/requirements/dev.txt index 6278834b98f..6539579cac5 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -107,7 +107,7 @@ markupsafe==3.0.2 # via jinja2 mdurl==0.1.2 # via markdown-it-py -multidict==6.2.0 +multidict==6.3.1 # via # -r requirements/runtime-deps.in # yarl diff --git a/requirements/multidict.txt b/requirements/multidict.txt index be4d86595fc..4ee354b5aa0 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.2.0 +multidict==6.3.1 # via -r requirements/multidict.in typing-extensions==4.12.2 # via multidict diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index 98a18d26c84..af4f3f89da8 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -22,7 +22,7 @@ frozenlist==1.5.0 # aiosignal idna==3.6 # via yarl -multidict==6.2.0 +multidict==6.3.1 # via # -r requirements/runtime-deps.in # yarl diff --git a/requirements/test.txt b/requirements/test.txt index 3e473970035..37838131042 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -55,7 +55,7 @@ markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -multidict==6.2.0 +multidict==6.3.1 # via # -r requirements/runtime-deps.in # yarl