diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 9d2b8b9f27f..61fb230371c 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -49,7 +49,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: 3.13 + python-version: 3.11 - name: Cache PyPI uses: actions/cache@v4.2.0 with: diff --git a/.mypy.ini b/.mypy.ini index 4a1333c4704..54efdae5bad 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -11,8 +11,9 @@ disallow_untyped_calls = True disallow_untyped_decorators = True disallow_untyped_defs = True # TODO(PY312): explicit-override -enable_error_code = ignore-without-code, possibly-undefined, redundant-expr, redundant-self, truthy-bool, truthy-iterable, unused-awaitable +enable_error_code = deprecated, ignore-without-code, possibly-undefined, redundant-expr, redundant-self, truthy-bool, truthy-iterable, unused-awaitable extra_checks = True +follow_untyped_imports = True implicit_reexport = False no_implicit_optional = True pretty = True diff --git a/CHANGES/10422.misc.rst b/CHANGES/10422.misc.rst new file mode 100644 index 00000000000..7ecb1c0e2e2 --- /dev/null +++ b/CHANGES/10422.misc.rst @@ -0,0 +1,3 @@ +Added human-readable error messages to the exceptions for WebSocket disconnects due to PONG not being received -- by :user:`bdraco`. + +Previously, the error messages were empty strings, which made it hard to determine what went wrong. diff --git a/CHANGES/10423.packaging.rst b/CHANGES/10423.packaging.rst new file mode 100644 index 00000000000..6cf58c5a10b --- /dev/null +++ b/CHANGES/10423.packaging.rst @@ -0,0 +1 @@ +Fixed test ``test_write_large_payload_deflate_compression_data_in_eof_writelines`` failing with Python 3.12.9+ or 3.13.2+ -- by :user:`bdraco`. diff --git a/aiohttp/client_ws.py b/aiohttp/client_ws.py index 757f5f0388f..bfd53ea64bf 100644 --- a/aiohttp/client_ws.py +++ b/aiohttp/client_ws.py @@ -163,7 +163,9 @@ def _ping_task_done(self, task: "asyncio.Task[None]") -> None: self._ping_task = None def _pong_not_received(self) -> None: - self._handle_ping_pong_exception(ServerTimeoutError()) + self._handle_ping_pong_exception( + ServerTimeoutError(f"No PONG received after {self._pong_heartbeat} seconds") + ) def _handle_ping_pong_exception(self, exc: BaseException) -> None: """Handle exceptions raised during ping/pong processing.""" diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py index d44f4cb12f1..bb78aea2fcd 100644 --- a/aiohttp/http_parser.py +++ b/aiohttp/http_parser.py @@ -670,7 +670,6 @@ def feed_data( ) -> Tuple[List[Tuple[RawResponseMessage, StreamReader]], bool, bytes]: if SEP is None: SEP = b"\r\n" if DEBUG else b"\n" - assert SEP is not None return super().feed_data(data, SEP, *args, **kwargs) def parse_message(self, lines: List[bytes]) -> RawResponseMessage: diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py index 613ef91d65e..4dbda424bd2 100644 --- a/aiohttp/web_protocol.py +++ b/aiohttp/web_protocol.py @@ -583,13 +583,13 @@ async def start(self) -> None: else: task = loop.create_task(coro) try: - resp, reset = await task # type: ignore[possibly-undefined] + resp, reset = await task except ConnectionError: self.log_debug("Ignored premature client disconnection") break # Drop the processed task from asyncio.Task.all_tasks() early - del task # type: ignore[possibly-undefined] + del task # https://github.com/python/mypy/issues/14309 if reset: # type: ignore[possibly-undefined] self.log_debug("Ignored premature client disconnection 2") diff --git a/aiohttp/web_ws.py b/aiohttp/web_ws.py index 4f104780c7d..813002e7871 100644 --- a/aiohttp/web_ws.py +++ b/aiohttp/web_ws.py @@ -186,7 +186,11 @@ def _ping_task_done(self, task: "asyncio.Task[None]") -> None: def _pong_not_received(self) -> None: if self._req is not None and self._req.transport is not None: - self._handle_ping_pong_exception(asyncio.TimeoutError()) + self._handle_ping_pong_exception( + asyncio.TimeoutError( + f"No PONG received after {self._pong_heartbeat} seconds" + ) + ) def _handle_ping_pong_exception(self, exc: BaseException) -> None: """Handle exceptions raised during ping/pong processing.""" diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 9bb9eb0b62c..37ad31c4391 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -108,7 +108,7 @@ multidict==6.1.0 # -r requirements/multidict.in # -r requirements/runtime-deps.in # yarl -mypy==1.14.1 ; implementation_name == "cpython" +mypy==1.15.0 ; implementation_name == "cpython" # via # -r requirements/lint.in # -r requirements/test.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 5250c375662..1af9a93415a 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -105,7 +105,7 @@ multidict==6.1.0 # via # -r requirements/runtime-deps.in # yarl -mypy==1.14.1 ; implementation_name == "cpython" +mypy==1.15.0 ; implementation_name == "cpython" # via # -r requirements/lint.in # -r requirements/test.in diff --git a/requirements/lint.txt b/requirements/lint.txt index 30c88c5761e..20c345649ca 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -39,7 +39,7 @@ markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -mypy==1.14.1 ; implementation_name == "cpython" +mypy==1.15.0 ; implementation_name == "cpython" # via -r requirements/lint.in mypy-extensions==1.0.0 # via mypy diff --git a/requirements/test.txt b/requirements/test.txt index 42b35e8d8df..93ab6812dd8 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -55,7 +55,7 @@ multidict==6.1.0 # via # -r requirements/runtime-deps.in # yarl -mypy==1.14.1 ; implementation_name == "cpython" +mypy==1.15.0 ; implementation_name == "cpython" # via -r requirements/test.in mypy-extensions==1.0.0 # via mypy diff --git a/tests/test_benchmarks_client.py b/tests/test_benchmarks_client.py index 61439183334..ac3131e9750 100644 --- a/tests/test_benchmarks_client.py +++ b/tests/test_benchmarks_client.py @@ -124,7 +124,7 @@ def test_one_hundred_get_requests_with_512kib_chunked_payload( aiohttp_client: AiohttpClient, benchmark: BenchmarkFixture, ) -> None: - """Benchmark 100 GET requests with a payload of 512KiB.""" + """Benchmark 100 GET requests with a payload of 512KiB using read.""" message_count = 100 payload = b"a" * (2**19) @@ -148,6 +148,36 @@ def _run() -> None: loop.run_until_complete(run_client_benchmark()) +def test_one_hundred_get_requests_iter_chunks_on_512kib_chunked_payload( + loop: asyncio.AbstractEventLoop, + aiohttp_client: AiohttpClient, + benchmark: BenchmarkFixture, +) -> None: + """Benchmark 100 GET requests with a payload of 512KiB using iter_chunks.""" + message_count = 100 + payload = b"a" * (2**19) + + async def handler(request: web.Request) -> web.Response: + resp = web.Response(body=payload) + resp.enable_chunked_encoding() + return resp + + app = web.Application() + app.router.add_route("GET", "/", handler) + + 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_chunks(): + pass + await client.close() + + @benchmark + def _run() -> None: + loop.run_until_complete(run_client_benchmark()) + + def test_get_request_with_251308_compressed_chunked_payload( loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient, diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 6c8bbca634f..414ee1dd51b 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -3477,20 +3477,6 @@ async def handler(request: web.Request) -> web.Response: assert resp.status == 200 -async def test_close_context_manager(aiohttp_client: AiohttpClient) -> None: - # a test for backward compatibility with yield from syntax - async def handler(request: web.Request) -> NoReturn: - assert False - - app = web.Application() - app.router.add_get("/", handler) - - client = await aiohttp_client(app) - ctx = client.get("/") - ctx.close() - assert not ctx._coro.cr_running - - async def test_session_auth(aiohttp_client: AiohttpClient) -> None: async def handler(request: web.Request) -> web.Response: return web.json_response({"headers": dict(request.headers)}) diff --git a/tests/test_client_ws_functional.py b/tests/test_client_ws_functional.py index 42c705ffb96..190096bbf5e 100644 --- a/tests/test_client_ws_functional.py +++ b/tests/test_client_ws_functional.py @@ -926,6 +926,7 @@ async def handler(request: web.Request) -> NoReturn: assert resp.close_code is WSCloseCode.ABNORMAL_CLOSURE assert msg.type is WSMsgType.ERROR assert isinstance(msg.data, ServerTimeoutError) + assert str(msg.data) == "No PONG received after 0.05 seconds" async def test_close_websocket_while_ping_inflight( diff --git a/tests/test_http_writer.py b/tests/test_http_writer.py index 95dabeab377..b76cac0fe58 100644 --- a/tests/test_http_writer.py +++ b/tests/test_http_writer.py @@ -19,6 +19,12 @@ def enable_writelines() -> Generator[None, None, None]: yield +@pytest.fixture +def disable_writelines() -> Generator[None, None, None]: + with mock.patch("aiohttp.http_writer.SKIP_WRITELINES", True): + yield + + @pytest.fixture def force_writelines_small_payloads() -> Generator[None, None, None]: with mock.patch("aiohttp.http_writer.MIN_PAYLOAD_FOR_WRITELINES", 1): @@ -123,6 +129,7 @@ async def test_write_payload_length( assert b"da" == content.split(b"\r\n\r\n", 1)[-1] +@pytest.mark.usefixtures("disable_writelines") async def test_write_large_payload_deflate_compression_data_in_eof( protocol: BaseProtocol, transport: asyncio.Transport, diff --git a/tests/test_web_websocket_functional.py b/tests/test_web_websocket_functional.py index 43e98e9f453..6bdd5808362 100644 --- a/tests/test_web_websocket_functional.py +++ b/tests/test_web_websocket_functional.py @@ -841,6 +841,7 @@ async def handler(request: web.Request) -> NoReturn: assert ws.close_code == WSCloseCode.ABNORMAL_CLOSURE assert ws_server_close_code == WSCloseCode.ABNORMAL_CLOSURE assert isinstance(ws_server_exception, asyncio.TimeoutError) + assert str(ws_server_exception) == "No PONG received after 0.025 seconds" await ws.close()