From 60bffb410dcdd46ec96d843854edec44e8c05557 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 28 Sep 2026 04:48:54 +0100 Subject: [PATCH] Switch PyPy testing to 3.12 (#13840) --- .github/workflows/ci-cd.yml | 2 +- aiohttp/connector.py | 4 ++-- aiohttp/payload.py | 4 ++-- requirements/constraints.txt | 4 ++-- requirements/dev.txt | 4 ++-- requirements/test-common.in | 4 ++-- requirements/test-common.txt | 4 ++-- requirements/test-ft.txt | 4 ++-- requirements/test.txt | 4 ++-- tests/test_client_functional.py | 9 +++++---- tests/test_connector.py | 26 ++++++++++++++++++++++++++ tests/test_multipart.py | 3 ++- tests/test_proxy.py | 18 +++++++++++++++++- tests/test_web_functional.py | 7 +++++-- 14 files changed, 72 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index a3a088f17f1..dc340634be4 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -275,7 +275,7 @@ jobs: - os: windows no-extensions: 'Y' include: - - pyver: pypy-3.11 + - pyver: pypy-3.12 no-extensions: 'Y' os: ubuntu experimental: false diff --git a/aiohttp/connector.py b/aiohttp/connector.py index 005e117a5be..c171656693c 100644 --- a/aiohttp/connector.py +++ b/aiohttp/connector.py @@ -144,7 +144,7 @@ async def start_tls( server_hostname: str | None, ssl_handshake_timeout: float | None, ssl_shutdown_timeout: float | None = None, -) -> asyncio.BaseTransport | None: +) -> asyncio.Transport | None: if aiofastnet is not None: return await aiofastnet.start_tls( loop, @@ -1473,7 +1473,7 @@ async def _start_tls_connection( else: underlying_transport.close() raise - if isinstance(tls_transport, asyncio.Transport): + if tls_transport is not None: fingerprint = self._get_fingerprint(req) if fingerprint: try: diff --git a/aiohttp/payload.py b/aiohttp/payload.py index 64cfacd2a40..f0fa00c1171 100644 --- a/aiohttp/payload.py +++ b/aiohttp/payload.py @@ -844,8 +844,8 @@ class BytesIOPayload(IOBasePayload): def __init__(self, value: io.BytesIO, *args: Any, **kwargs: Any) -> None: super().__init__(value, *args, **kwargs) - # Calculate size once during initialization - self._size = len(self._value.getbuffer()) - self._value.tell() + with self._value.getbuffer() as buf: + self._size = len(buf) - self._value.tell() @property def size(self) -> int: diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 8dfe975c2e2..5a0fff5255a 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -239,7 +239,7 @@ python-dateutil==2.9.0.post0 # via freezegun python-discovery==1.6.1 # via virtualenv -python-on-whales==0.81.0 +python-on-whales==0.81.0 ; implementation_name == "cpython" # via # -r requirements/lint.in # -r requirements/test-common.in @@ -306,7 +306,7 @@ towncrier==26.9.0 # via # -r requirements/doc.in # sphinxcontrib-towncrier -trustme==1.2.1 ; platform_machine != "i686" +trustme==1.2.1 ; platform_machine != "i686" 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 74456608e4b..e6cfed71ab9 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -234,7 +234,7 @@ python-dateutil==2.9.0.post0 # via freezegun python-discovery==1.6.1 # via virtualenv -python-on-whales==0.81.0 +python-on-whales==0.81.0 ; implementation_name == "cpython" # via # -r requirements/lint.in # -r requirements/test-common.in @@ -296,7 +296,7 @@ towncrier==26.9.0 # via # -r requirements/doc.in # sphinxcontrib-towncrier -trustme==1.2.1 ; platform_machine != "i686" +trustme==1.2.1 ; platform_machine != "i686" 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 d3aa5d156b0..da63994b5c4 100644 --- a/requirements/test-common.in +++ b/requirements/test-common.in @@ -6,6 +6,6 @@ isal; python_version < "3.14" and implementation_name == "cpython" # no wheel fo mypy; implementation_name == "cpython" pytest-xdist pytest_codspeed -python-on-whales -trustme; platform_machine != "i686" # no 32-bit wheels +python-on-whales; implementation_name == "cpython" # Used for autobahn, not run on PyPy +trustme; platform_machine != "i686" and implementation_name == "cpython" # TODO: include on PyPy once new release is out zlib_ng diff --git a/requirements/test-common.txt b/requirements/test-common.txt index c88c4bc4c60..b58416da14c 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -114,7 +114,7 @@ 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 +python-on-whales==0.81.0 ; implementation_name == "cpython" # via -r requirements/test-common.in rich==15.0.0 # via pytest-codspeed @@ -127,7 +127,7 @@ tomli==2.4.1 # coverage # mypy # pytest -trustme==1.2.1 ; platform_machine != "i686" +trustme==1.2.1 ; platform_machine != "i686" and implementation_name == "cpython" # via -r requirements/test-common.in typing-extensions==4.16.0 # via diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 1f47cc63304..3de4a44acb4 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -139,7 +139,7 @@ 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 +python-on-whales==0.81.0 ; implementation_name == "cpython" # via -r requirements/test-common.in rich==15.0.0 # via pytest-codspeed @@ -152,7 +152,7 @@ tomli==2.4.1 # coverage # mypy # pytest -trustme==1.2.1 ; platform_machine != "i686" +trustme==1.2.1 ; platform_machine != "i686" and implementation_name == "cpython" # via -r requirements/test-common.in typing-extensions==4.16.0 ; python_version < "3.13" # via diff --git a/requirements/test.txt b/requirements/test.txt index ba602c82cf2..f52243ade12 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -139,7 +139,7 @@ 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 +python-on-whales==0.81.0 ; implementation_name == "cpython" # via -r requirements/test-common.in rich==15.0.0 # via pytest-codspeed @@ -152,7 +152,7 @@ tomli==2.4.1 # coverage # mypy # pytest -trustme==1.2.1 ; platform_machine != "i686" +trustme==1.2.1 ; platform_machine != "i686" and implementation_name == "cpython" # via -r requirements/test-common.in typing-extensions==4.16.0 ; python_version < "3.13" # via diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 340bb2b29f3..fedb42d2390 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -295,7 +295,8 @@ async def handler(request: web.Request) -> web.Response: app = web.Application() app.router.add_route("GET", "/", handler) - runner = web.AppRunner(app, tcp_keepalive=True, keepalive_timeout=0.001) + # Timeout must be enough to outlive first request on slow platforms. + runner = web.AppRunner(app, tcp_keepalive=True, keepalive_timeout=0.2) await runner.setup() site = web.SockSite(runner, unused_port_socket) @@ -308,7 +309,7 @@ async def handler(request: web.Request) -> web.Response: resp1 = await sess.get(f"http://{host}:{port}/") await resp1.read() # wait for server keepalive_timeout - await asyncio.sleep(0.01) + await asyncio.sleep(0.5) resp2 = await sess.get(f"http://{host}:{port}/") await resp2.read() finally: @@ -328,7 +329,7 @@ async def handler(request: web.Request) -> web.Response: app = web.Application() app.router.add_route("GET", "/", handler) - runner = web.AppRunner(app, tcp_keepalive=True, keepalive_timeout=0.001) + runner = web.AppRunner(app, tcp_keepalive=True, keepalive_timeout=0.2) await runner.setup() site = web.SockSite(runner, unused_port_socket) @@ -342,7 +343,7 @@ async def handler(request: web.Request) -> web.Response: await resp1.read() # wait for server keepalive_timeout # time.sleep is a more challenging scenario than asyncio.sleep - time.sleep(0.01) + time.sleep(0.5) resp2 = await sess.get(f"http://{host}:{port}/") await resp2.read() finally: diff --git a/tests/test_connector.py b/tests/test_connector.py index 85ddf8c75c4..cdecff2c2e4 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -2678,6 +2678,32 @@ async def test_start_tls_exception_with_ssl_shutdown_timeout_nonzero_pre_311() - underlying_transport.abort.assert_not_called() +async def test_start_tls_connection_returns_none( + make_client_request: _RequestMaker, +) -> None: + """A transport closed before the upgrade makes start_tls() return None.""" + loop = asyncio.get_running_loop() + conn = aiohttp.TCPConnector() + req = make_client_request("GET", URL("https://example.com"), loop=loop) + + with socket.socket() as listener: + listener.bind(("127.0.0.1", 0)) + listener.listen() + transport, _ = await loop.create_connection( + asyncio.Protocol, *listener.getsockname() + ) + transport.close() + + # start_tls() returns None on asyncio, but not on aiofastnet. + with mock.patch.object(connector_module, "aiofastnet", None): + with pytest.raises(aiohttp.ClientConnectorError) as exc_info: + await conn._start_tls_connection(transport, req, ClientTimeout()) + + assert "Failed to start TLS" in exc_info.value.os_error.args[0] + + await conn.close() + + def test_client_timeout_total_zero_raises() -> None: """Test that ClientTimeout(total=0) raises ValueError. diff --git a/tests/test_multipart.py b/tests/test_multipart.py index d5c3c00592d..184b766f5f4 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -87,7 +87,8 @@ async def read(self, size: int | None = None) -> bytes: return self.content.read(size) def at_eof(self) -> bool: - return self.content.tell() == len(self.content.getbuffer()) + with self.content.getbuffer() as buf: + return self.content.tell() == len(buf) async def readline(self, *, max_line_length: int | None = None) -> bytes: return self.content.readline() diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 39e5ee561ce..616770d4f91 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -403,10 +403,16 @@ async def test_proxy_server_hostname_override( # type: ignore[misc] ) @pytest.mark.usefixtures("enable_cleanup_closed") @pytest.mark.parametrize("cleanup", (True, False)) +@pytest.mark.parametrize( + "asyncio_transport", + (True, False), + ids=("asyncio-transport", "duck-typed-transport"), +) async def test_https_connect_fingerprint_mismatch( # type: ignore[misc] start_connection: mock.Mock, ClientRequestMock: mock.Mock, cleanup: bool, + asyncio_transport: bool, make_client_request: _RequestMaker, ) -> None: event_loop = asyncio.get_running_loop() @@ -423,6 +429,16 @@ class TransportMock(asyncio.Transport): def close(self) -> None: pass + class DuckTypedTransportMock: + """Models aiofastnet's transport, which subclasses no asyncio class.""" + + def close(self) -> None: + pass + + transport_mock: object = ( + TransportMock() if asyncio_transport else DuckTypedTransportMock() + ) + url = URL("http://proxy.example.com") proxy_resp = ClientResponse( "get", @@ -497,7 +513,7 @@ def close(self) -> None: "start_tls", autospec=True, spec_set=True, - return_value=TransportMock(), + return_value=transport_mock, ), ): req = make_client_request( diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py index de1eee569b9..204677d42bd 100644 --- a/tests/test_web_functional.py +++ b/tests/test_web_functional.py @@ -2061,7 +2061,10 @@ async def ws_handler(request: web.Request) -> web.WebSocketResponse: for _ in range(frames): await ws_writer.send_frame(frame_payload.encode(), WSMsgType.TEXT) - writer.write(encoded) + # Write an immutable snapshot: zero-copy selector transports hold + # buffer exports of the written object until flushed, which would + # make the clear() below raise BufferError. + writer.write(bytes(encoded)) await writer.drain() await asyncio.wait_for(reading_paused.wait(), 5) @@ -2069,7 +2072,7 @@ async def ws_handler(request: web.Request) -> web.WebSocketResponse: # handover to the websocket resumes the transport. encoded.clear() await ws_writer.send_frame(b"last", WSMsgType.TEXT) - writer.write(encoded) + writer.write(bytes(encoded)) await writer.drain() release_handler.set()