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: 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/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 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_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 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()