From 5f3e508407fc599602ac825e5662ff56e0769c17 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 14 Dec 2024 18:06:40 +0100 Subject: [PATCH 1/6] Drop Python 3.8 --- .github/workflows/test-suite.yml | 2 +- pyproject.toml | 5 +---- uvicorn/server.py | 5 +---- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 6f7e2db8f..7646f10c9 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] os: [windows-latest, ubuntu-latest, macos-latest] steps: - uses: "actions/checkout@v4" diff --git a/pyproject.toml b/pyproject.toml index 6f809030e..3a4bdfd6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ dynamic = ["version"] description = "The lightning-fast ASGI server." readme = "README.md" license = "BSD-3-Clause" -requires-python = ">=3.8" +requires-python = ">=3.9" authors = [ { name = "Tom Christie", email = "tom@tomchristie.com" }, { name = "Marcelo Trylesinski", email = "marcelotryle@gmail.com" }, @@ -20,7 +20,6 @@ classifiers = [ "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -127,8 +126,6 @@ py-win32 = "sys_platform == 'win32'" py-not-win32 = "sys_platform != 'win32'" py-linux = "sys_platform == 'linux'" py-darwin = "sys_platform == 'darwin'" -py-gte-38 = "sys_version_info >= (3, 8)" -py-lt-38 = "sys_version_info < (3, 8)" py-gte-39 = "sys_version_info >= (3, 9)" py-lt-39 = "sys_version_info < (3, 9)" py-gte-310 = "sys_version_info >= (3, 10)" diff --git a/uvicorn/server.py b/uvicorn/server.py index f14026f16..6b2a4ae1c 100644 --- a/uvicorn/server.py +++ b/uvicorn/server.py @@ -284,10 +284,7 @@ async def shutdown(self, sockets: list[socket.socket] | None = None) -> None: len(self.server_state.tasks), ) for t in self.server_state.tasks: - if sys.version_info < (3, 9): # pragma: py-gte-39 - t.cancel() - else: # pragma: py-lt-39 - t.cancel(msg="Task cancelled, timeout graceful shutdown exceeded") + t.cancel(msg="Task cancelled, timeout graceful shutdown exceeded") # Send the lifespan shutdown event, and wait for application shutdown. if not self.force_exit: From 70562c71402309eef9cefb2ba8cdd206b44e90ab Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 14 Dec 2024 18:18:59 +0100 Subject: [PATCH 2/6] Drop Python 3.8 --- requirements.txt | 8 +- tests/middleware/test_logging.py | 9 +- tests/middleware/test_proxy_headers.py | 4 +- tests/protocols/test_websocket.py | 110 +++++++++--------- .../protocols/websockets/websockets_impl.py | 5 +- 5 files changed, 70 insertions(+), 66 deletions(-) diff --git a/requirements.txt b/requirements.txt index b3a464c0b..b16569f3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ h11 @ git+https://github.com/python-hyper/h11.git@master # Explicit optionals a2wsgi==1.10.7 wsproto==1.2.0 -websockets==13.1 +websockets==14.1 # Packaging build==1.2.2.post1 @@ -20,11 +20,9 @@ pytest-mock==3.14.0 mypy==1.13.0 types-click==7.1.8 types-pyyaml==6.0.12.20240917 -trustme==1.1.0; python_version < '3.9' -trustme==1.2.0; python_version >= '3.9' +trustme==1.2.0 cryptography==44.0.0 -coverage==7.6.1; python_version < '3.9' -coverage==7.6.9; python_version >= '3.9' +coverage==7.6.9 coverage-conditional-plugin==0.9.0 httpx==0.28.1 diff --git a/tests/middleware/test_logging.py b/tests/middleware/test_logging.py index f27633aa5..920433e5a 100644 --- a/tests/middleware/test_logging.py +++ b/tests/middleware/test_logging.py @@ -8,8 +8,7 @@ import httpx import pytest -import websockets -import websockets.client +from websockets.asyncio.client import connect from tests.utils import run_server from uvicorn import Config @@ -104,9 +103,9 @@ async def websocket_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISe elif message["type"] == "websocket.disconnect": break - async def open_connection(url): - async with websockets.client.connect(url) as websocket: - return websocket.open + async def open_connection(url: str): + async with connect(url): + return True config = Config( app=websocket_app, diff --git a/tests/middleware/test_proxy_headers.py b/tests/middleware/test_proxy_headers.py index 0ade97450..7ba9631c6 100644 --- a/tests/middleware/test_proxy_headers.py +++ b/tests/middleware/test_proxy_headers.py @@ -5,7 +5,7 @@ import httpx import httpx._transports.asgi import pytest -import websockets.client +from websockets.asyncio.client import connect from tests.response import Response from tests.utils import run_server @@ -478,7 +478,7 @@ async def websocket_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISe async with run_server(config): url = f"ws://127.0.0.1:{unused_tcp_port}" headers = {X_FORWARDED_FOR: "1.2.3.4", X_FORWARDED_PROTO: forwarded_proto} - async with websockets.client.connect(url, extra_headers=headers) as websocket: + async with connect(url, additional_headers=headers) as websocket: data = await websocket.recv() assert data == expected diff --git a/tests/protocols/test_websocket.py b/tests/protocols/test_websocket.py index 15ccfdd7d..a02872179 100644 --- a/tests/protocols/test_websocket.py +++ b/tests/protocols/test_websocket.py @@ -7,9 +7,10 @@ import httpx import pytest import websockets -import websockets.client import websockets.exceptions from typing_extensions import TypedDict +from websockets import WebSocketClientProtocol +from websockets.asyncio.client import connect from websockets.extensions.permessage_deflate import ClientPerMessageDeflateFactory from websockets.typing import Subprotocol @@ -128,8 +129,8 @@ async def websocket_connect(self, message: WebSocketConnectEvent): await self.send({"type": "websocket.accept"}) async def open_connection(url: str): - async with websockets.client.connect(url) as websocket: - return websocket.open + async with connect(url): + return True config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): @@ -144,7 +145,7 @@ async def websocket_connect(self, message: WebSocketConnectEvent): config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config) as server: - async with websockets.client.connect(f"ws://127.0.0.1:{unused_tcp_port}"): + async with connect(f"ws://127.0.0.1:{unused_tcp_port}"): # Attempt shutdown while connection is still open await server.shutdown() @@ -158,8 +159,8 @@ async def websocket_connect(self, message: WebSocketConnectEvent): async def open_connection(url: str): extension_factories = [ClientPerMessageDeflateFactory()] - async with websockets.client.connect(url, extensions=extension_factories) as websocket: - return [extension.name for extension in websocket.extensions] + async with connect(url, extensions=extension_factories) as websocket: + return [extension.name for extension in websocket.protocol.extensions] config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): @@ -178,8 +179,8 @@ async def open_connection(url: str): # enable per-message deflate on the client, so that we can check the server # won't support it when it's disabled. extension_factories = [ClientPerMessageDeflateFactory()] - async with websockets.client.connect(url, extensions=extension_factories) as websocket: - return [extension.name for extension in websocket.extensions] + async with connect(url, extensions=extension_factories) as websocket: + return [extension.name for extension in websocket.protocol.extensions] config = Config( app=App, @@ -201,7 +202,7 @@ async def websocket_connect(self, message: WebSocketConnectEvent): async def open_connection(url: str): try: - await websockets.client.connect(url) + await connect(url) except websockets.exceptions.InvalidHandshake: return False return True # pragma: no cover @@ -222,8 +223,8 @@ async def websocket_connect(self, message: WebSocketConnectEvent): await self.send({"type": "websocket.accept"}) async def open_connection(url: str): - async with websockets.client.connect(url, extra_headers=[("username", "abraão")]) as websocket: - return websocket.open + async with connect(url, extra_headers=[("username", "abraão")]): + return True config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): @@ -237,8 +238,9 @@ async def websocket_connect(self, message: WebSocketConnectEvent): await self.send({"type": "websocket.accept", "headers": [(b"extra", b"header")]}) async def open_connection(url: str): - async with websockets.client.connect(url) as websocket: - return websocket.response_headers + async with connect(url) as websocket: + assert websocket.response + return websocket.response.headers config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): @@ -256,8 +258,8 @@ async def websocket_connect(self, message: WebSocketConnectEvent): await self.send({"type": "websocket.accept"}) async def open_connection(url: str): - async with websockets.client.connect(url) as websocket: - return websocket.open + async with connect(url): + return True config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): @@ -274,7 +276,7 @@ async def websocket_connect(self, message: WebSocketConnectEvent): await self.send({"type": "websocket.send", "text": "123"}) async def get_data(url: str): - async with websockets.client.connect(url) as websocket: + async with connect(url) as websocket: return await websocket.recv() config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) @@ -292,7 +294,7 @@ async def websocket_connect(self, message: WebSocketConnectEvent): await self.send({"type": "websocket.send", "bytes": b"123"}) async def get_data(url: str): - async with websockets.client.connect(url) as websocket: + async with connect(url) as websocket: return await websocket.recv() config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) @@ -311,7 +313,7 @@ async def websocket_connect(self, message: WebSocketConnectEvent): await self.send({"type": "websocket.close"}) async def get_data(url: str): - async with websockets.client.connect(url) as websocket: + async with connect(url) as websocket: data = await websocket.recv() is_open = True try: @@ -340,7 +342,7 @@ async def websocket_receive(self, message: WebSocketReceiveEvent): await self.send({"type": "websocket.send", "text": _text}) async def send_text(url: str): - async with websockets.client.connect(url) as websocket: + async with connect(url) as websocket: await websocket.send("abc") return await websocket.recv() @@ -363,7 +365,7 @@ async def websocket_receive(self, message: WebSocketReceiveEvent): await self.send({"type": "websocket.send", "bytes": _bytes}) async def send_text(url: str): - async with websockets.client.connect(url) as websocket: + async with connect(url) as websocket: await websocket.send(b"abc") return await websocket.recv() @@ -385,7 +387,7 @@ async def websocket_connect(self, message: WebSocketConnectEvent): await self.send({"type": "websocket.send", "text": "123"}) async def get_data(url: str): - async with websockets.client.connect(url) as websocket: + async with connect(url) as websocket: data = await websocket.recv() is_open = True try: @@ -405,13 +407,13 @@ async def test_missing_handshake(ws_protocol_cls: WSProtocol, http_protocol_cls: async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable): pass - async def connect(url: str): - await websockets.client.connect(url) + async def open_connection(url: str): + await connect(url) config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info: - await connect(f"ws://127.0.0.1:{unused_tcp_port}") + await open_connection(f"ws://127.0.0.1:{unused_tcp_port}") assert exc_info.value.status_code == 500 @@ -421,13 +423,13 @@ async def test_send_before_handshake( async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable): await send({"type": "websocket.send", "text": "123"}) - async def connect(url: str): - await websockets.client.connect(url) + async def open_connection(url: str): + await connect(url) config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info: - await connect(f"ws://127.0.0.1:{unused_tcp_port}") + await open_connection(f"ws://127.0.0.1:{unused_tcp_port}") assert exc_info.value.status_code == 500 @@ -438,10 +440,10 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): - async with websockets.client.connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: - with pytest.raises(websockets.exceptions.ConnectionClosed): + async with connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: + with pytest.raises(websockets.exceptions.ConnectionClosed) as exc_info: _ = await websocket.recv() - assert websocket.close_code == 1006 + assert exc_info.value.code == 1006 async def test_asgi_return_value(ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int): @@ -456,10 +458,10 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): - async with websockets.client.connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: - with pytest.raises(websockets.exceptions.ConnectionClosed): + async with connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: + with pytest.raises(websockets.exceptions.ConnectionClosed) as exc_info: _ = await websocket.recv() - assert websocket.close_code == 1006 + assert exc_info.value.code == 1006 @pytest.mark.parametrize("code", [None, 1000, 1001]) @@ -646,7 +648,7 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable disconnect_message = message break - websocket: websockets.client.WebSocketClientProtocol | None = None + websocket: WebSocketClientProtocol | None = None async def websocket_session(uri: str): nonlocal websocket @@ -735,15 +737,15 @@ async def websocket_receive(self, message: WebSocketReceiveEvent): port=unused_tcp_port, ) async with run_server(config): - async with websockets.client.connect(f"ws://127.0.0.1:{unused_tcp_port}", max_size=client_size_sent) as ws: + async with connect(f"ws://127.0.0.1:{unused_tcp_port}", max_size=client_size_sent) as ws: await ws.send(b"\x01" * client_size_sent) if expected_result == 0: data = await ws.recv() assert data == b"\x01" * client_size_sent else: - with pytest.raises(websockets.exceptions.ConnectionClosedError): + with pytest.raises(websockets.exceptions.ConnectionClosedError) as exc_info: await ws.recv() - assert ws.close_code == expected_result + assert exc_info.value.code == expected_result async def test_server_reject_connection( @@ -769,7 +771,7 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable async def websocket_session(url: str): with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info: - async with websockets.client.connect(url): + async with connect(url): pass # pragma: no cover assert exc_info.value.status_code == 403 @@ -939,7 +941,7 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable async def websocket_session(url: str): with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info: - async with websockets.client.connect(url): + async with connect(url): pass # pragma: no cover assert exc_info.value.status_code == 404 @@ -970,7 +972,7 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable async def websocket_session(url: str): with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info: - async with websockets.client.connect(url): + async with connect(url): pass # pragma: no cover assert exc_info.value.status_code == 404 @@ -1011,7 +1013,7 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable async def websocket_session(url: str): with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info: - async with websockets.client.connect(url): + async with connect(url): pass # pragma: no cover assert exc_info.value.status_code == 404 @@ -1049,7 +1051,7 @@ async def websocket_receive(self, message: WebSocketReceiveEvent): config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): - async with websockets.client.connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: + async with connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: await websocket.send(b"abc") await websocket.send(b"abc") await websocket.send(b"abc") @@ -1066,8 +1068,9 @@ async def websocket_connect(self, message: WebSocketConnectEvent): await self.send({"type": "websocket.accept"}) async def open_connection(url: str): - async with websockets.client.connect(url) as websocket: - return websocket.response_headers + async with connect(url) as websocket: + assert websocket.response + return websocket.response.headers config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): @@ -1081,8 +1084,9 @@ async def websocket_connect(self, message: WebSocketConnectEvent): await self.send({"type": "websocket.accept"}) async def open_connection(url: str): - async with websockets.client.connect(url) as websocket: - return websocket.response_headers + async with connect(url) as websocket: + assert websocket.response + return websocket.response.headers config = Config( app=App, @@ -1104,8 +1108,9 @@ async def websocket_connect(self, message: WebSocketConnectEvent): await self.send({"type": "websocket.accept"}) async def open_connection(url: str): - async with websockets.client.connect(url) as websocket: - return websocket.response_headers + async with connect(url) as websocket: + assert websocket.response + return websocket.response.headers config = Config( app=App, @@ -1136,8 +1141,9 @@ async def websocket_connect(self, message: WebSocketConnectEvent): ) async def open_connection(url: str): - async with websockets.client.connect(url) as websocket: - return websocket.response_headers + async with connect(url) as websocket: + assert websocket.response + return websocket.response.headers config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): @@ -1172,8 +1178,8 @@ async def websocket_connect(self, message: WebSocketConnectEvent): await self.send({"type": "websocket.accept"}) async def open_connection(url: str): - async with websockets.client.connect(url) as websocket: - return websocket.open + async with connect(url): + return True async def app_wrapper(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable): if scope["type"] == "lifespan": diff --git a/uvicorn/protocols/websockets/websockets_impl.py b/uvicorn/protocols/websockets/websockets_impl.py index af66c29b3..f504da64a 100644 --- a/uvicorn/protocols/websockets/websockets_impl.py +++ b/uvicorn/protocols/websockets/websockets_impl.py @@ -3,7 +3,8 @@ import asyncio import http import logging -from typing import Any, Literal, Optional, Sequence, cast +from collections.abc import Sequence +from typing import Any, Literal, Optional, cast from urllib.parse import unquote import websockets @@ -94,7 +95,7 @@ def __init__( self.lost_connection_before_handshake = False self.accepted_subprotocol: Subprotocol | None = None - self.ws_server: Server = Server() # type: ignore[assignment] + self.ws_server = Server() extensions: list[ServerExtensionFactory] = [] if self.config.ws_per_message_deflate: From 90f536ea2153d345ab31719b9affd1ee96d38be9 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 14 Dec 2024 22:31:32 +0100 Subject: [PATCH 3/6] try to drop it... --- pyproject.toml | 3 + tests/protocols/test_websocket.py | 61 +++++++++---------- .../protocols/websockets/websockets_impl.py | 5 +- 3 files changed, 34 insertions(+), 35 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3a4bdfd6f..dc4b9ab04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,6 +92,9 @@ filterwarnings = [ "ignore:Uvicorn's native WSGI implementation is deprecated.*:DeprecationWarning", "ignore: 'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning", "ignore: remove second argument of ws_handler:DeprecationWarning:websockets", + "ignore: websockets.legacy is deprecated.*:DeprecationWarning:websockets", + "ignore: websockets.server.WebSocketServerProtocol is deprecated.*:DeprecationWarning:websockets", + "ignore: websockets.exceptions.InvalidStatusCode.*:DeprecationWarning", ] [tool.coverage.run] diff --git a/tests/protocols/test_websocket.py b/tests/protocols/test_websocket.py index a02872179..0de4c7ead 100644 --- a/tests/protocols/test_websocket.py +++ b/tests/protocols/test_websocket.py @@ -7,10 +7,9 @@ import httpx import pytest import websockets -import websockets.exceptions from typing_extensions import TypedDict -from websockets import WebSocketClientProtocol -from websockets.asyncio.client import connect +from websockets.asyncio.client import ClientConnection, connect +from websockets.exceptions import ConnectionClosed, ConnectionClosedError, InvalidHandshake, InvalidStatus from websockets.extensions.permessage_deflate import ClientPerMessageDeflateFactory from websockets.typing import Subprotocol @@ -203,7 +202,7 @@ async def websocket_connect(self, message: WebSocketConnectEvent): async def open_connection(url: str): try: await connect(url) - except websockets.exceptions.InvalidHandshake: + except InvalidHandshake: return False return True # pragma: no cover @@ -412,9 +411,9 @@ async def open_connection(url: str): config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): - with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info: + with pytest.raises(InvalidStatus) as exc_info: await open_connection(f"ws://127.0.0.1:{unused_tcp_port}") - assert exc_info.value.status_code == 500 + assert exc_info.value.response.status_code == 500 async def test_send_before_handshake( @@ -428,9 +427,9 @@ async def open_connection(url: str): config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): - with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info: + with pytest.raises(InvalidStatus) as exc_info: await open_connection(f"ws://127.0.0.1:{unused_tcp_port}") - assert exc_info.value.status_code == 500 + assert exc_info.value.response.status_code == 500 async def test_duplicate_handshake(ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int): @@ -441,9 +440,9 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): async with connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: - with pytest.raises(websockets.exceptions.ConnectionClosed) as exc_info: + with pytest.raises(ConnectionClosed): _ = await websocket.recv() - assert exc_info.value.code == 1006 + assert websocket.protocol.close_code == 1006 async def test_asgi_return_value(ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int): @@ -459,7 +458,7 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): async with connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: - with pytest.raises(websockets.exceptions.ConnectionClosed) as exc_info: + with pytest.raises(ConnectionClosed) as exc_info: _ = await websocket.recv() assert exc_info.value.code == 1006 @@ -493,13 +492,13 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): - async with websockets.client.connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: + async with connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: await websocket.ping() await websocket.send("abc") - with pytest.raises(websockets.exceptions.ConnectionClosed): + with pytest.raises(ConnectionClosed) as exc_info: await websocket.recv() - assert websocket.close_code == (code or 1000) - assert websocket.close_reason == (reason or "") + assert exc_info.value.code == (code or 1000) + assert exc_info.value.reason == (reason or "") async def test_client_close(ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int): @@ -555,7 +554,7 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable port=unused_tcp_port, ) async with run_server(config): - async with websockets.client.connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: + async with connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: websocket.transport.close() await asyncio.sleep(0.1) got_disconnect_event_before_shutdown = got_disconnect_event @@ -583,7 +582,7 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): url = f"ws://127.0.0.1:{unused_tcp_port}" - async with websockets.client.connect(url): + async with connect(url): await asyncio.sleep(0.1) disconnect.set() @@ -648,11 +647,11 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable disconnect_message = message break - websocket: WebSocketClientProtocol | None = None + websocket: ClientConnection | None = None async def websocket_session(uri: str): nonlocal websocket - async with websockets.client.connect(uri) as ws_connection: + async with connect(uri) as ws_connection: websocket = ws_connection await server_shutdown_event.wait() @@ -682,9 +681,7 @@ async def websocket_connect(self, message: WebSocketConnectEvent): await self.send({"type": "websocket.accept", "subprotocol": subprotocol}) async def get_subprotocol(url: str): - async with websockets.client.connect( - url, subprotocols=[Subprotocol("proto1"), Subprotocol("proto2")] - ) as websocket: + async with connect(url, subprotocols=[Subprotocol("proto1"), Subprotocol("proto2")]) as websocket: return websocket.subprotocol config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) @@ -743,9 +740,9 @@ async def websocket_receive(self, message: WebSocketReceiveEvent): data = await ws.recv() assert data == b"\x01" * client_size_sent else: - with pytest.raises(websockets.exceptions.ConnectionClosedError) as exc_info: + with pytest.raises(ConnectionClosedError): await ws.recv() - assert exc_info.value.code == expected_result + assert ws.protocol.close_code == expected_result async def test_server_reject_connection( @@ -770,10 +767,10 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable disconnected_message = await receive() async def websocket_session(url: str): - with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info: + with pytest.raises(InvalidStatus) as exc_info: async with connect(url): pass # pragma: no cover - assert exc_info.value.status_code == 403 + assert exc_info.value.response.status_code == 403 config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): @@ -940,10 +937,10 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable await send(message) async def websocket_session(url: str): - with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info: + with pytest.raises(InvalidStatus) as exc_info: async with connect(url): pass # pragma: no cover - assert exc_info.value.status_code == 404 + assert exc_info.value.response.status_code == 404 config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): @@ -971,10 +968,10 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable # no further message async def websocket_session(url: str): - with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info: + with pytest.raises(InvalidStatus) as exc_info: async with connect(url): pass # pragma: no cover - assert exc_info.value.status_code == 404 + assert exc_info.value.response.status_code == 404 config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): @@ -1012,10 +1009,10 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable exception_message = str(exc) async def websocket_session(url: str): - with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info: + with pytest.raises(InvalidStatus) as exc_info: async with connect(url): pass # pragma: no cover - assert exc_info.value.status_code == 404 + assert exc_info.value.response.status_code == 404 config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): diff --git a/uvicorn/protocols/websockets/websockets_impl.py b/uvicorn/protocols/websockets/websockets_impl.py index f504da64a..9f3d9eb57 100644 --- a/uvicorn/protocols/websockets/websockets_impl.py +++ b/uvicorn/protocols/websockets/websockets_impl.py @@ -13,8 +13,7 @@ from websockets.exceptions import ConnectionClosed from websockets.extensions.base import ServerExtensionFactory from websockets.extensions.permessage_deflate import ServerPerMessageDeflateFactory -from websockets.legacy.server import HTTPResponse -from websockets.server import WebSocketServerProtocol +from websockets.legacy.server import HTTPResponse, WebSocketServerProtocol from websockets.typing import Subprotocol from uvicorn._types import ( @@ -95,7 +94,7 @@ def __init__( self.lost_connection_before_handshake = False self.accepted_subprotocol: Subprotocol | None = None - self.ws_server = Server() + self.ws_server: Server = Server() extensions: list[ServerExtensionFactory] = [] if self.config.ws_per_message_deflate: From c7498b3cba0b724d8aee8095520fccd039c102d8 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 14 Dec 2024 23:13:22 +0100 Subject: [PATCH 4/6] Drop Python 3.8 --- tests/protocols/test_websocket.py | 19 +++++++++---------- uvicorn/protocols/websockets/wsproto_impl.py | 17 ++++++++++------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/tests/protocols/test_websocket.py b/tests/protocols/test_websocket.py index 0de4c7ead..9d708c411 100644 --- a/tests/protocols/test_websocket.py +++ b/tests/protocols/test_websocket.py @@ -6,7 +6,6 @@ import httpx import pytest -import websockets from typing_extensions import TypedDict from websockets.asyncio.client import ClientConnection, connect from websockets.exceptions import ConnectionClosed, ConnectionClosedError, InvalidHandshake, InvalidStatus @@ -222,7 +221,7 @@ async def websocket_connect(self, message: WebSocketConnectEvent): await self.send({"type": "websocket.accept"}) async def open_connection(url: str): - async with connect(url, extra_headers=[("username", "abraão")]): + async with connect(url, additional_headers=[("username", "abraão")]): return True config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) @@ -458,9 +457,9 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) async with run_server(config): async with connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: - with pytest.raises(ConnectionClosed) as exc_info: + with pytest.raises(ConnectionClosed): _ = await websocket.recv() - assert exc_info.value.code == 1006 + assert websocket.protocol.close_code == 1006 @pytest.mark.parametrize("code", [None, 1000, 1001]) @@ -495,10 +494,10 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable async with connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: await websocket.ping() await websocket.send("abc") - with pytest.raises(ConnectionClosed) as exc_info: + with pytest.raises(ConnectionClosed): await websocket.recv() - assert exc_info.value.code == (code or 1000) - assert exc_info.value.reason == (reason or "") + assert websocket.protocol.close_code == (code or 1000) + assert websocket.protocol.close_reason == (reason or "") async def test_client_close(ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int): @@ -517,7 +516,7 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable break async def websocket_session(url: str): - async with websockets.client.connect(url) as websocket: + async with connect(url) as websocket: await websocket.ping() await websocket.send("abc") await websocket.close(code=1001, reason="custom reason") @@ -691,7 +690,7 @@ async def get_subprotocol(url: str): MAX_WS_BYTES = 1024 * 1024 * 16 -MAX_WS_BYTES_PLUS1 = MAX_WS_BYTES + 1 +MAX_WS_BYTES_PLUS1 = MAX_WS_BYTES + 10 @pytest.mark.parametrize( @@ -1019,7 +1018,7 @@ async def websocket_session(url: str): await websocket_session(f"ws://127.0.0.1:{unused_tcp_port}") assert exception_message == ( - "Expected ASGI message 'websocket.http.response.body' but got " "'websocket.http.response.start'." + "Expected ASGI message 'websocket.http.response.body' but got 'websocket.http.response.start'." ) diff --git a/uvicorn/protocols/websockets/wsproto_impl.py b/uvicorn/protocols/websockets/wsproto_impl.py index 072dec942..fe25fe88d 100644 --- a/uvicorn/protocols/websockets/wsproto_impl.py +++ b/uvicorn/protocols/websockets/wsproto_impl.py @@ -149,12 +149,13 @@ def resume_writing(self) -> None: self.writable.set() # pragma: full coverage def shutdown(self) -> None: - if self.handshake_complete: - self.queue.put_nowait({"type": "websocket.disconnect", "code": 1012}) - output = self.conn.send(wsproto.events.CloseConnection(code=1012)) - self.transport.write(output) - else: - self.send_500_response() + if not self.response_started: + if self.handshake_complete: + self.queue.put_nowait({"type": "websocket.disconnect", "code": 1012}) + output = self.conn.send(wsproto.events.CloseConnection(code=1012)) + self.transport.write(output) + else: + self.send_500_response() self.transport.close() def on_task_complete(self, task: asyncio.Task[None]) -> None: @@ -221,12 +222,14 @@ def handle_ping(self, event: events.Ping) -> None: def send_500_response(self) -> None: if self.response_started or self.handshake_complete: return # we cannot send responses anymore + reject_data = b"Internal Server Error" headers: list[tuple[bytes, bytes]] = [ (b"content-type", b"text/plain; charset=utf-8"), + (b"content-length", str(len(reject_data)).encode()), (b"connection", b"close"), ] output = self.conn.send(wsproto.events.RejectConnection(status_code=500, headers=headers, has_body=True)) - output += self.conn.send(wsproto.events.RejectData(data=b"Internal Server Error")) + output += self.conn.send(wsproto.events.RejectData(data=reject_data)) self.transport.write(output) async def run_asgi(self) -> None: From 2af31eb8bbf126b6b3cb134f156c7caa362ac1f0 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 14 Dec 2024 23:23:17 +0100 Subject: [PATCH 5/6] readd type ignore --- uvicorn/protocols/websockets/websockets_impl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uvicorn/protocols/websockets/websockets_impl.py b/uvicorn/protocols/websockets/websockets_impl.py index 9f3d9eb57..685d6b659 100644 --- a/uvicorn/protocols/websockets/websockets_impl.py +++ b/uvicorn/protocols/websockets/websockets_impl.py @@ -94,7 +94,7 @@ def __init__( self.lost_connection_before_handshake = False self.accepted_subprotocol: Subprotocol | None = None - self.ws_server: Server = Server() + self.ws_server: Server = Server() # type: ignore[assignment] extensions: list[ServerExtensionFactory] = [] if self.config.ws_per_message_deflate: From b0bd5e87a3bd98c85491ce6edbe6bae59d05b721 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 14 Dec 2024 23:28:58 +0100 Subject: [PATCH 6/6] fix lint --- tests/middleware/test_wsgi.py | 3 ++- tests/supervisors/test_reload.py | 3 ++- tests/test_cli.py | 2 +- tests/test_server.py | 6 ++++-- uvicorn/_types.py | 22 +++++----------------- uvicorn/config.py | 3 ++- uvicorn/middleware/wsgi.py | 2 +- uvicorn/server.py | 3 ++- uvicorn/supervisors/basereload.py | 3 ++- uvicorn/supervisors/statreload.py | 3 ++- 10 files changed, 23 insertions(+), 27 deletions(-) diff --git a/tests/middleware/test_wsgi.py b/tests/middleware/test_wsgi.py index 69a40db8c..6003f27f9 100644 --- a/tests/middleware/test_wsgi.py +++ b/tests/middleware/test_wsgi.py @@ -2,7 +2,8 @@ import io import sys -from typing import AsyncGenerator, Callable +from collections.abc import AsyncGenerator +from typing import Callable import a2wsgi import httpx diff --git a/tests/supervisors/test_reload.py b/tests/supervisors/test_reload.py index c4ad76acb..44eb9970b 100644 --- a/tests/supervisors/test_reload.py +++ b/tests/supervisors/test_reload.py @@ -4,10 +4,11 @@ import signal import socket import sys +from collections.abc import Generator from pathlib import Path from threading import Thread from time import sleep -from typing import Callable, Generator +from typing import Callable import pytest from pytest_mock import MockerFixture diff --git a/tests/test_cli.py b/tests/test_cli.py index 8c54e6d19..303ae6feb 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -3,9 +3,9 @@ import os import platform import sys +from collections.abc import Iterator from pathlib import Path from textwrap import dedent -from typing import Iterator from unittest import mock import pytest diff --git a/tests/test_server.py b/tests/test_server.py index c650be290..d14206eb5 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -5,7 +5,9 @@ import logging import signal import sys -from typing import Callable, ContextManager, Generator +from collections.abc import Generator +from contextlib import AbstractContextManager +from typing import Callable import httpx import pytest @@ -62,7 +64,7 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable @pytest.mark.parametrize("exception_signal", signals) @pytest.mark.parametrize("capture_signal", signal_captures) async def test_server_interrupt( - exception_signal: signal.Signals, capture_signal: Callable[[signal.Signals], ContextManager[None]] + exception_signal: signal.Signals, capture_signal: Callable[[signal.Signals], AbstractContextManager[None]] ): # pragma: py-win32 """Test interrupting a Server that is run explicitly inside asyncio""" diff --git a/uvicorn/_types.py b/uvicorn/_types.py index 8c8065ae3..c927cc11d 100644 --- a/uvicorn/_types.py +++ b/uvicorn/_types.py @@ -32,20 +32,8 @@ import sys import types -from typing import ( - Any, - Awaitable, - Callable, - Iterable, - Literal, - MutableMapping, - Optional, - Protocol, - Tuple, - Type, - TypedDict, - Union, -) +from collections.abc import Awaitable, Iterable, MutableMapping +from typing import Any, Callable, Literal, Optional, Protocol, TypedDict, Union if sys.version_info >= (3, 11): # pragma: py-lt-311 from typing import NotRequired @@ -54,8 +42,8 @@ # WSGI Environ = MutableMapping[str, Any] -ExcInfo = Tuple[Type[BaseException], BaseException, Optional[types.TracebackType]] -StartResponse = Callable[[str, Iterable[Tuple[str, str]], Optional[ExcInfo]], None] +ExcInfo = tuple[type[BaseException], BaseException, Optional[types.TracebackType]] +StartResponse = Callable[[str, Iterable[tuple[str, str]], Optional[ExcInfo]], None] WSGIApp = Callable[[Environ, StartResponse], Union[Iterable[bytes], BaseException]] @@ -281,7 +269,7 @@ def __init__(self, scope: Scope) -> None: ... # pragma: no cover async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... # pragma: no cover -ASGI2Application = Type[ASGI2Protocol] +ASGI2Application = type[ASGI2Protocol] ASGI3Application = Callable[ [ Scope, diff --git a/uvicorn/config.py b/uvicorn/config.py index b08a8426b..664d1918f 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -9,9 +9,10 @@ import socket import ssl import sys +from collections.abc import Awaitable from configparser import RawConfigParser from pathlib import Path -from typing import IO, Any, Awaitable, Callable, Literal +from typing import IO, Any, Callable, Literal import click diff --git a/uvicorn/middleware/wsgi.py b/uvicorn/middleware/wsgi.py index 078de1af0..2193e8194 100644 --- a/uvicorn/middleware/wsgi.py +++ b/uvicorn/middleware/wsgi.py @@ -6,7 +6,7 @@ import sys import warnings from collections import deque -from typing import Iterable +from collections.abc import Iterable from uvicorn._types import ( ASGIReceiveCallable, diff --git a/uvicorn/server.py b/uvicorn/server.py index 6b2a4ae1c..cca2e850c 100644 --- a/uvicorn/server.py +++ b/uvicorn/server.py @@ -10,9 +10,10 @@ import sys import threading import time +from collections.abc import Generator, Sequence from email.utils import formatdate from types import FrameType -from typing import TYPE_CHECKING, Generator, Sequence, Union +from typing import TYPE_CHECKING, Union import click diff --git a/uvicorn/supervisors/basereload.py b/uvicorn/supervisors/basereload.py index f07ca3912..4df50af33 100644 --- a/uvicorn/supervisors/basereload.py +++ b/uvicorn/supervisors/basereload.py @@ -5,10 +5,11 @@ import signal import sys import threading +from collections.abc import Iterator from pathlib import Path from socket import socket from types import FrameType -from typing import Callable, Iterator +from typing import Callable import click diff --git a/uvicorn/supervisors/statreload.py b/uvicorn/supervisors/statreload.py index 70d0a6d5c..bdcdaa0bf 100644 --- a/uvicorn/supervisors/statreload.py +++ b/uvicorn/supervisors/statreload.py @@ -1,9 +1,10 @@ from __future__ import annotations import logging +from collections.abc import Iterator from pathlib import Path from socket import socket -from typing import Callable, Iterator +from typing import Callable from uvicorn.config import Config from uvicorn.supervisors.basereload import BaseReload