uvicorn 0.34.2__tar.gz → 0.35.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. {uvicorn-0.34.2 → uvicorn-0.35.0}/PKG-INFO +2 -2
  2. {uvicorn-0.34.2 → uvicorn-0.35.0}/pyproject.toml +4 -1
  3. {uvicorn-0.34.2 → uvicorn-0.35.0}/requirements.txt +5 -6
  4. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/conftest.py +2 -2
  5. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/middleware/test_logging.py +14 -11
  6. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/middleware/test_proxy_headers.py +1 -0
  7. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/protocols/test_websocket.py +14 -12
  8. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/supervisors/test_reload.py +6 -14
  9. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/test_config.py +6 -6
  10. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/__init__.py +1 -1
  11. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/config.py +3 -2
  12. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/main.py +1 -1
  13. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/protocols/websockets/auto.py +2 -2
  14. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/protocols/websockets/websockets_impl.py +4 -3
  15. uvicorn-0.35.0/uvicorn/protocols/websockets/websockets_sansio_impl.py +417 -0
  16. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/protocols/websockets/wsproto_impl.py +11 -11
  17. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/server.py +2 -1
  18. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/supervisors/watchfilesreload.py +1 -4
  19. {uvicorn-0.34.2 → uvicorn-0.35.0}/.gitignore +0 -0
  20. {uvicorn-0.34.2 → uvicorn-0.35.0}/LICENSE.md +0 -0
  21. {uvicorn-0.34.2 → uvicorn-0.35.0}/README.md +0 -0
  22. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/__init__.py +0 -0
  23. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/importer/__init__.py +0 -0
  24. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/importer/circular_import_a.py +0 -0
  25. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/importer/circular_import_b.py +0 -0
  26. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/importer/raise_import_error.py +0 -0
  27. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/importer/test_importer.py +0 -0
  28. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/middleware/__init__.py +0 -0
  29. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/middleware/test_message_logger.py +0 -0
  30. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/middleware/test_wsgi.py +0 -0
  31. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/protocols/__init__.py +0 -0
  32. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/protocols/test_http.py +0 -0
  33. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/protocols/test_utils.py +0 -0
  34. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/response.py +0 -0
  35. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/supervisors/__init__.py +0 -0
  36. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/supervisors/test_multiprocess.py +0 -0
  37. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/supervisors/test_signal.py +0 -0
  38. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/test_auto_detection.py +0 -0
  39. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/test_cli.py +0 -0
  40. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/test_default_headers.py +0 -0
  41. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/test_lifespan.py +0 -0
  42. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/test_main.py +0 -0
  43. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/test_server.py +0 -0
  44. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/test_ssl.py +0 -0
  45. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/test_subprocess.py +0 -0
  46. {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/utils.py +0 -0
  47. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/__main__.py +0 -0
  48. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/_subprocess.py +0 -0
  49. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/_types.py +0 -0
  50. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/importer.py +0 -0
  51. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/lifespan/__init__.py +0 -0
  52. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/lifespan/off.py +0 -0
  53. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/lifespan/on.py +0 -0
  54. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/logging.py +0 -0
  55. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/loops/__init__.py +0 -0
  56. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/loops/asyncio.py +0 -0
  57. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/loops/auto.py +0 -0
  58. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/loops/uvloop.py +0 -0
  59. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/middleware/__init__.py +0 -0
  60. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/middleware/asgi2.py +0 -0
  61. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/middleware/message_logger.py +0 -0
  62. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/middleware/proxy_headers.py +0 -0
  63. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/middleware/wsgi.py +0 -0
  64. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/protocols/__init__.py +0 -0
  65. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/protocols/http/__init__.py +0 -0
  66. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/protocols/http/auto.py +0 -0
  67. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/protocols/http/flow_control.py +0 -0
  68. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/protocols/http/h11_impl.py +0 -0
  69. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/protocols/http/httptools_impl.py +0 -0
  70. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/protocols/utils.py +0 -0
  71. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/protocols/websockets/__init__.py +0 -0
  72. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/py.typed +0 -0
  73. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/supervisors/__init__.py +0 -0
  74. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/supervisors/basereload.py +0 -0
  75. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/supervisors/multiprocess.py +0 -0
  76. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/supervisors/statreload.py +0 -0
  77. {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/workers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: uvicorn
3
- Version: 0.34.2
3
+ Version: 0.35.0
4
4
  Summary: The lightning-fast ASGI server.
5
5
  Project-URL: Changelog, https://www.uvicorn.org/release-notes
6
6
  Project-URL: Funding, https://github.com/sponsors/encode
@@ -32,7 +32,7 @@ Requires-Dist: colorama>=0.4; (sys_platform == 'win32') and extra == 'standard'
32
32
  Requires-Dist: httptools>=0.6.3; extra == 'standard'
33
33
  Requires-Dist: python-dotenv>=0.13; extra == 'standard'
34
34
  Requires-Dist: pyyaml>=5.1; extra == 'standard'
35
- Requires-Dist: uvloop!=0.15.0,!=0.15.1,>=0.14.0; (sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy')) and extra == 'standard'
35
+ Requires-Dist: uvloop>=0.15.1; (sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy')) and extra == 'standard'
36
36
  Requires-Dist: watchfiles>=0.13; extra == 'standard'
37
37
  Requires-Dist: websockets>=10.4; extra == 'standard'
38
38
  Description-Content-Type: text/markdown
@@ -41,7 +41,7 @@ standard = [
41
41
  "httptools>=0.6.3",
42
42
  "python-dotenv>=0.13",
43
43
  "PyYAML>=5.1",
44
- "uvloop>=0.14.0,!=0.15.0,!=0.15.1; sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy')",
44
+ "uvloop>=0.15.1; sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy')",
45
45
  "watchfiles>=0.13",
46
46
  "websockets>=10.4",
47
47
  ]
@@ -92,6 +92,9 @@ filterwarnings = [
92
92
  "ignore:Uvicorn's native WSGI implementation is deprecated.*:DeprecationWarning",
93
93
  "ignore: 'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning",
94
94
  "ignore: remove second argument of ws_handler:DeprecationWarning:websockets",
95
+ "ignore: websockets.legacy is deprecated.*:DeprecationWarning",
96
+ "ignore: websockets.server.WebSocketServerProtocol is deprecated.*:DeprecationWarning",
97
+ "ignore: websockets.client.connect is deprecated.*:DeprecationWarning",
95
98
  ]
96
99
 
97
100
  [tool.coverage.run]
@@ -1,8 +1,7 @@
1
1
  -e .[standard]
2
2
 
3
- # TODO: Remove this after h11 makes a release. By this writing, h11 was on 0.14.0.
4
3
  # Core dependencies
5
- h11 @ git+https://github.com/python-hyper/h11.git@master
4
+ h11==0.16.0
6
5
 
7
6
  # Explicit optionals
8
7
  a2wsgi==1.10.8
@@ -14,15 +13,15 @@ build==1.2.2.post1
14
13
  twine==6.1.0
15
14
 
16
15
  # Testing
17
- ruff==0.11.2
16
+ ruff==0.11.9
18
17
  pytest==8.3.5
19
18
  pytest-mock==3.14.0
20
19
  pytest-xdist[psutil]==3.6.1
21
20
  mypy==1.15.0
22
21
  types-click==7.1.8
23
- types-pyyaml==6.0.12.20250326
22
+ types-pyyaml==6.0.12.20250402
24
23
  trustme==1.2.1
25
- cryptography==44.0.2
24
+ cryptography==44.0.3
26
25
  coverage==7.8.0
27
26
  coverage-conditional-plugin==0.9.0
28
27
  coverage-enable-subprocess==1.0
@@ -30,4 +29,4 @@ httpx==0.28.1
30
29
 
31
30
  # Documentation
32
31
  mkdocs==1.6.1
33
- mkdocs-material==9.6.10
32
+ mkdocs-material==9.6.13
@@ -233,9 +233,9 @@ def unused_tcp_port() -> int:
233
233
  marks=pytest.mark.skipif(not importlib.util.find_spec("wsproto"), reason="wsproto not installed."),
234
234
  id="wsproto",
235
235
  ),
236
+ pytest.param("uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol", id="websockets"),
236
237
  pytest.param(
237
- "uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol",
238
- id="websockets",
238
+ "uvicorn.protocols.websockets.websockets_sansio_impl:WebSocketsSansIOProtocol", id="websockets-sansio"
239
239
  ),
240
240
  ]
241
241
  )
@@ -4,18 +4,19 @@ import contextlib
4
4
  import logging
5
5
  import socket
6
6
  import sys
7
- import typing
7
+ from collections.abc import Iterator
8
+ from typing import TYPE_CHECKING, Any
8
9
 
9
10
  import httpx
10
11
  import pytest
11
- import websockets
12
12
  import websockets.client
13
+ from websockets.protocol import State
13
14
 
14
15
  from tests.utils import run_server
15
16
  from uvicorn import Config
16
17
  from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope
17
18
 
18
- if typing.TYPE_CHECKING:
19
+ if TYPE_CHECKING:
19
20
  import sys
20
21
 
21
22
  from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol
@@ -32,7 +33,7 @@ pytestmark = pytest.mark.anyio
32
33
 
33
34
 
34
35
  @contextlib.contextmanager
35
- def caplog_for_logger(caplog: pytest.LogCaptureFixture, logger_name: str) -> typing.Iterator[pytest.LogCaptureFixture]:
36
+ def caplog_for_logger(caplog: pytest.LogCaptureFixture, logger_name: str) -> Iterator[pytest.LogCaptureFixture]:
36
37
  logger = logging.getLogger(logger_name)
37
38
  logger.propagate, old_propagate = False, logger.propagate
38
39
  logger.addHandler(caplog.handler)
@@ -49,7 +50,7 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable
49
50
  await send({"type": "http.response.body", "body": b"", "more_body": False})
50
51
 
51
52
 
52
- async def test_trace_logging(caplog: pytest.LogCaptureFixture, logging_config, unused_tcp_port: int):
53
+ async def test_trace_logging(caplog: pytest.LogCaptureFixture, logging_config: dict[str, Any], unused_tcp_port: int):
53
54
  config = Config(
54
55
  app=app,
55
56
  log_level="trace",
@@ -91,8 +92,8 @@ async def test_trace_logging_on_http_protocol(http_protocol_cls, caplog, logging
91
92
 
92
93
  async def test_trace_logging_on_ws_protocol(
93
94
  ws_protocol_cls: WSProtocol,
94
- caplog,
95
- logging_config,
95
+ caplog: pytest.LogCaptureFixture,
96
+ logging_config: dict[str, Any],
96
97
  unused_tcp_port: int,
97
98
  ):
98
99
  async def websocket_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
@@ -104,9 +105,9 @@ async def test_trace_logging_on_ws_protocol(
104
105
  elif message["type"] == "websocket.disconnect":
105
106
  break
106
107
 
107
- async def open_connection(url):
108
+ async def open_connection(url: str):
108
109
  async with websockets.client.connect(url) as websocket:
109
- return websocket.open
110
+ return websocket.state is State.OPEN
110
111
 
111
112
  config = Config(
112
113
  app=websocket_app,
@@ -126,7 +127,9 @@ async def test_trace_logging_on_ws_protocol(
126
127
 
127
128
 
128
129
  @pytest.mark.parametrize("use_colors", [(True), (False), (None)])
129
- async def test_access_logging(use_colors: bool, caplog: pytest.LogCaptureFixture, logging_config, unused_tcp_port: int):
130
+ async def test_access_logging(
131
+ use_colors: bool, caplog: pytest.LogCaptureFixture, logging_config: dict[str, Any], unused_tcp_port: int
132
+ ):
130
133
  config = Config(app=app, use_colors=use_colors, log_config=logging_config, port=unused_tcp_port)
131
134
  with caplog_for_logger(caplog, "uvicorn.access"):
132
135
  async with run_server(config):
@@ -140,7 +143,7 @@ async def test_access_logging(use_colors: bool, caplog: pytest.LogCaptureFixture
140
143
 
141
144
  @pytest.mark.parametrize("use_colors", [(True), (False)])
142
145
  async def test_default_logging(
143
- use_colors: bool, caplog: pytest.LogCaptureFixture, logging_config, unused_tcp_port: int
146
+ use_colors: bool, caplog: pytest.LogCaptureFixture, logging_config: dict[str, Any], unused_tcp_port: int
144
147
  ):
145
148
  config = Config(app=app, use_colors=use_colors, log_config=logging_config, port=unused_tcp_port)
146
149
  with caplog_for_logger(caplog, "uvicorn.access"):
@@ -465,6 +465,7 @@ async def test_proxy_headers_websocket_x_forwarded_proto(
465
465
  host, port = scope["client"]
466
466
  await send({"type": "websocket.accept"})
467
467
  await send({"type": "websocket.send", "text": f"{scheme}://{host}:{port}"})
468
+ await send({"type": "websocket.close"})
468
469
 
469
470
  app_with_middleware = ProxyHeadersMiddleware(websocket_app, trusted_hosts="*")
470
471
  config = Config(
@@ -1,8 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import typing
5
4
  from copy import deepcopy
5
+ from typing import TYPE_CHECKING, Any, TypedDict
6
6
 
7
7
  import httpx
8
8
  import pytest
@@ -35,7 +35,7 @@ try:
35
35
  except ModuleNotFoundError: # pragma: no cover
36
36
  skip_if_no_wsproto = pytest.mark.skipif(True, reason="wsproto is not installed.")
37
37
 
38
- if typing.TYPE_CHECKING:
38
+ if TYPE_CHECKING:
39
39
  import sys
40
40
 
41
41
  from uvicorn.protocols.http.h11_impl import H11Protocol
@@ -597,12 +597,9 @@ async def test_connection_lost_before_handshake_complete(
597
597
  await send_accept_task.wait()
598
598
  disconnect_message = await receive() # type: ignore
599
599
 
600
- response: httpx.Response | None = None
601
-
602
600
  async def websocket_session(uri: str):
603
- nonlocal response
604
601
  async with httpx.AsyncClient() as client:
605
- response = await client.get(
602
+ await client.get(
606
603
  f"http://127.0.0.1:{unused_tcp_port}",
607
604
  headers={
608
605
  "upgrade": "websocket",
@@ -619,9 +616,6 @@ async def test_connection_lost_before_handshake_complete(
619
616
  send_accept_task.set()
620
617
  await asyncio.sleep(0.1)
621
618
 
622
- assert response is not None
623
- assert response.status_code == 500, response.text
624
- assert response.text == "Internal Server Error"
625
619
  assert disconnect_message == {"type": "websocket.disconnect", "code": 1006}
626
620
  await task
627
621
 
@@ -776,7 +770,7 @@ async def test_server_reject_connection(
776
770
  assert disconnected_message == {"type": "websocket.disconnect", "code": 1006}
777
771
 
778
772
 
779
- class EmptyDict(typing.TypedDict): ...
773
+ class EmptyDict(TypedDict): ...
780
774
 
781
775
 
782
776
  async def test_server_reject_connection_with_response(
@@ -916,6 +910,9 @@ async def test_server_reject_connection_with_body_nolength(
916
910
  async def test_server_reject_connection_with_invalid_msg(
917
911
  ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int
918
912
  ):
913
+ if ws_protocol_cls.__name__ == "WebSocketsSansIOProtocol":
914
+ pytest.skip("WebSocketsSansIOProtocol sends both start and body messages in one message.")
915
+
919
916
  async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
920
917
  assert scope["type"] == "websocket"
921
918
  assert "extensions" in scope and "websocket.http.response" in scope["extensions"]
@@ -947,6 +944,9 @@ async def test_server_reject_connection_with_invalid_msg(
947
944
  async def test_server_reject_connection_with_missing_body(
948
945
  ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int
949
946
  ):
947
+ if ws_protocol_cls.__name__ == "WebSocketsSansIOProtocol":
948
+ pytest.skip("WebSocketsSansIOProtocol sends both start and body messages in one message.")
949
+
950
950
  async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
951
951
  assert scope["type"] == "websocket"
952
952
  assert "extensions" in scope and "websocket.http.response" in scope["extensions"]
@@ -982,6 +982,8 @@ async def test_server_multiple_websocket_http_response_start_events(
982
982
  The server should raise an exception if it sends multiple
983
983
  websocket.http.response.start events.
984
984
  """
985
+ if ws_protocol_cls.__name__ == "WebSocketsSansIOProtocol":
986
+ pytest.skip("WebSocketsSansIOProtocol sends both start and body messages in one message.")
985
987
  exception_message: str | None = None
986
988
 
987
989
  async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
@@ -1142,12 +1144,12 @@ async def test_multiple_server_header(
1142
1144
 
1143
1145
 
1144
1146
  async def test_lifespan_state(ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int):
1145
- expected_states: list[dict[str, typing.Any]] = [
1147
+ expected_states: list[dict[str, Any]] = [
1146
1148
  {"a": 123, "b": [1]},
1147
1149
  {"a": 123, "b": [1, 2]},
1148
1150
  ]
1149
1151
 
1150
- actual_states: list[dict[str, typing.Any]] = []
1152
+ actual_states: list[dict[str, Any]] = []
1151
1153
 
1152
1154
  async def lifespan_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
1153
1155
  message = await receive()
@@ -306,24 +306,17 @@ class TestBaseReload:
306
306
 
307
307
 
308
308
  @pytest.mark.skipif(WatchFilesReload is None, reason="watchfiles not available")
309
- def test_should_watch_one_dir_cwd(mocker: MockerFixture, reload_directory_structure: Path):
309
+ def test_should_watch_cwd(mocker: MockerFixture, reload_directory_structure: Path):
310
310
  mock_watch = mocker.patch("uvicorn.supervisors.watchfilesreload.watch")
311
- app_dir = reload_directory_structure / "app"
312
- app_first_dir = reload_directory_structure / "app_first"
313
311
 
314
- with as_cwd(reload_directory_structure):
315
- config = Config(
316
- app="tests.test_config:asgi_app",
317
- reload=True,
318
- reload_dirs=[str(app_dir), str(app_first_dir)],
319
- )
320
- WatchFilesReload(config, target=run, sockets=[])
321
- mock_watch.assert_called_once()
322
- assert mock_watch.call_args[0] == (Path.cwd(),)
312
+ config = Config(app="tests.test_config:asgi_app", reload=True, reload_dirs=[])
313
+ WatchFilesReload(config, target=run, sockets=[])
314
+ mock_watch.assert_called_once()
315
+ assert mock_watch.call_args[0] == (Path.cwd(),)
323
316
 
324
317
 
325
318
  @pytest.mark.skipif(WatchFilesReload is None, reason="watchfiles not available")
326
- def test_should_watch_separate_dirs_outside_cwd(mocker: MockerFixture, reload_directory_structure: Path):
319
+ def test_should_watch_multiple_dirs(mocker: MockerFixture, reload_directory_structure: Path):
327
320
  mock_watch = mocker.patch("uvicorn.supervisors.watchfilesreload.watch")
328
321
  app_dir = reload_directory_structure / "app"
329
322
  app_first_dir = reload_directory_structure / "app_first"
@@ -337,7 +330,6 @@ def test_should_watch_separate_dirs_outside_cwd(mocker: MockerFixture, reload_di
337
330
  assert set(mock_watch.call_args[0]) == {
338
331
  app_dir,
339
332
  app_first_dir,
340
- Path.cwd(),
341
333
  }
342
334
 
343
335
 
@@ -7,9 +7,9 @@ import logging
7
7
  import os
8
8
  import socket
9
9
  import sys
10
- import typing
10
+ from collections.abc import Iterator
11
11
  from pathlib import Path
12
- from typing import Any, Literal
12
+ from typing import IO, Any, Callable, Literal
13
13
  from unittest.mock import MagicMock
14
14
 
15
15
  import pytest
@@ -291,7 +291,7 @@ def test_ssl_config_combined(tls_certificate_key_and_chain_path: str) -> None:
291
291
  assert config.is_ssl is True
292
292
 
293
293
 
294
- def asgi2_app(scope: Scope) -> typing.Callable:
294
+ def asgi2_app(scope: Scope) -> Callable:
295
295
  async def asgi(receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: # pragma: nocover
296
296
  pass
297
297
 
@@ -374,7 +374,7 @@ def test_log_config_yaml(
374
374
  @pytest.mark.parametrize("config_file", ["log_config.ini", configparser.ConfigParser(), io.StringIO()])
375
375
  def test_log_config_file(
376
376
  mocked_logging_config_module: MagicMock,
377
- config_file: str | configparser.RawConfigParser | typing.IO[Any],
377
+ config_file: str | configparser.RawConfigParser | IO[Any],
378
378
  ) -> None:
379
379
  """
380
380
  Test that one can load a configparser config from disk.
@@ -386,14 +386,14 @@ def test_log_config_file(
386
386
 
387
387
 
388
388
  @pytest.fixture(params=[0, 1])
389
- def web_concurrency(request: pytest.FixtureRequest) -> typing.Iterator[int]:
389
+ def web_concurrency(request: pytest.FixtureRequest) -> Iterator[int]:
390
390
  yield request.param
391
391
  if os.getenv("WEB_CONCURRENCY"):
392
392
  del os.environ["WEB_CONCURRENCY"]
393
393
 
394
394
 
395
395
  @pytest.fixture(params=["127.0.0.1", "127.0.0.2"])
396
- def forwarded_allow_ips(request: pytest.FixtureRequest) -> typing.Iterator[str]:
396
+ def forwarded_allow_ips(request: pytest.FixtureRequest) -> Iterator[str]:
397
397
  yield request.param
398
398
  if os.getenv("FORWARDED_ALLOW_IPS"):
399
399
  del os.environ["FORWARDED_ALLOW_IPS"]
@@ -1,5 +1,5 @@
1
1
  from uvicorn.config import Config
2
2
  from uvicorn.main import Server, main, run
3
3
 
4
- __version__ = "0.34.2"
4
+ __version__ = "0.35.0"
5
5
  __all__ = ["main", "run", "Config", "Server"]
@@ -25,7 +25,7 @@ from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
25
25
  from uvicorn.middleware.wsgi import WSGIMiddleware
26
26
 
27
27
  HTTPProtocolType = Literal["auto", "h11", "httptools"]
28
- WSProtocolType = Literal["auto", "none", "websockets", "wsproto"]
28
+ WSProtocolType = Literal["auto", "none", "websockets", "websockets-sansio", "wsproto"]
29
29
  LifespanType = Literal["auto", "on", "off"]
30
30
  LoopSetupType = Literal["none", "auto", "asyncio", "uvloop"]
31
31
  InterfaceType = Literal["auto", "asgi3", "asgi2", "wsgi"]
@@ -47,6 +47,7 @@ WS_PROTOCOLS: dict[WSProtocolType, str | None] = {
47
47
  "auto": "uvicorn.protocols.websockets.auto:AutoWebSocketsProtocol",
48
48
  "none": None,
49
49
  "websockets": "uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol",
50
+ "websockets-sansio": "uvicorn.protocols.websockets.websockets_sansio_impl:WebSocketsSansIOProtocol",
50
51
  "wsproto": "uvicorn.protocols.websockets.wsproto_impl:WSProtocol",
51
52
  }
52
53
  LIFESPAN: dict[LifespanType, str] = {
@@ -313,7 +314,7 @@ class Config:
313
314
  + "directories, watching current working directory.",
314
315
  reload_dirs,
315
316
  )
316
- self.reload_dirs = [Path(os.getcwd())]
317
+ self.reload_dirs = [Path.cwd()]
317
318
 
318
319
  logger.info(
319
320
  "Will watch for changes in these directories: %s",
@@ -223,7 +223,7 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
223
223
  "--proxy-headers/--no-proxy-headers",
224
224
  is_flag=True,
225
225
  default=True,
226
- help="Enable/Disable X-Forwarded-Proto, X-Forwarded-For, X-Forwarded-Port to populate remote address info.",
226
+ help="Enable/Disable X-Forwarded-Proto, X-Forwarded-For to populate url scheme and remote address info.",
227
227
  )
228
228
  @click.option(
229
229
  "--server-header/--no-server-header",
@@ -1,9 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import typing
4
+ from typing import Callable
5
5
 
6
- AutoWebSocketsProtocol: typing.Callable[..., asyncio.Protocol] | None
6
+ AutoWebSocketsProtocol: Callable[..., asyncio.Protocol] | None
7
7
  try:
8
8
  import websockets # noqa
9
9
  except ImportError: # pragma: no cover
@@ -34,6 +34,7 @@ from uvicorn.config import Config
34
34
  from uvicorn.logging import TRACE_LOG_LEVEL
35
35
  from uvicorn.protocols.utils import (
36
36
  ClientDisconnected,
37
+ get_client_addr,
37
38
  get_local_addr,
38
39
  get_path_with_query_string,
39
40
  get_remote_addr,
@@ -271,7 +272,7 @@ class WebSocketProtocol(WebSocketServerProtocol):
271
272
  message = cast("WebSocketAcceptEvent", message)
272
273
  self.logger.info(
273
274
  '%s - "WebSocket %s" [accepted]',
274
- self.scope["client"],
275
+ get_client_addr(self.scope),
275
276
  get_path_with_query_string(self.scope),
276
277
  )
277
278
  self.initial_response = None
@@ -289,7 +290,7 @@ class WebSocketProtocol(WebSocketServerProtocol):
289
290
  message = cast("WebSocketCloseEvent", message)
290
291
  self.logger.info(
291
292
  '%s - "WebSocket %s" 403',
292
- self.scope["client"],
293
+ get_client_addr(self.scope),
293
294
  get_path_with_query_string(self.scope),
294
295
  )
295
296
  self.initial_response = (http.HTTPStatus.FORBIDDEN, [], b"")
@@ -300,7 +301,7 @@ class WebSocketProtocol(WebSocketServerProtocol):
300
301
  message = cast("WebSocketResponseStartEvent", message)
301
302
  self.logger.info(
302
303
  '%s - "WebSocket %s" %d',
303
- self.scope["client"],
304
+ get_client_addr(self.scope),
304
305
  get_path_with_query_string(self.scope),
305
306
  message["status"],
306
307
  )
@@ -0,0 +1,417 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import sys
6
+ from asyncio.transports import BaseTransport, Transport
7
+ from http import HTTPStatus
8
+ from typing import Any, Literal, cast
9
+ from urllib.parse import unquote
10
+
11
+ from websockets.exceptions import InvalidState
12
+ from websockets.extensions.permessage_deflate import ServerPerMessageDeflateFactory
13
+ from websockets.frames import Frame, Opcode
14
+ from websockets.http11 import Request
15
+ from websockets.server import ServerProtocol
16
+
17
+ from uvicorn._types import (
18
+ ASGIReceiveEvent,
19
+ ASGISendEvent,
20
+ WebSocketAcceptEvent,
21
+ WebSocketCloseEvent,
22
+ WebSocketResponseBodyEvent,
23
+ WebSocketResponseStartEvent,
24
+ WebSocketScope,
25
+ WebSocketSendEvent,
26
+ )
27
+ from uvicorn.config import Config
28
+ from uvicorn.logging import TRACE_LOG_LEVEL
29
+ from uvicorn.protocols.utils import (
30
+ ClientDisconnected,
31
+ get_local_addr,
32
+ get_path_with_query_string,
33
+ get_remote_addr,
34
+ is_ssl,
35
+ )
36
+ from uvicorn.server import ServerState
37
+
38
+ if sys.version_info >= (3, 11): # pragma: no cover
39
+ from typing import assert_never
40
+ else: # pragma: no cover
41
+ from typing_extensions import assert_never
42
+
43
+
44
+ class WebSocketsSansIOProtocol(asyncio.Protocol):
45
+ def __init__(
46
+ self,
47
+ config: Config,
48
+ server_state: ServerState,
49
+ app_state: dict[str, Any],
50
+ _loop: asyncio.AbstractEventLoop | None = None,
51
+ ) -> None:
52
+ if not config.loaded:
53
+ config.load() # pragma: no cover
54
+
55
+ self.config = config
56
+ self.app = config.loaded_app
57
+ self.loop = _loop or asyncio.get_event_loop()
58
+ self.logger = logging.getLogger("uvicorn.error")
59
+ self.root_path = config.root_path
60
+ self.app_state = app_state
61
+
62
+ # Shared server state
63
+ self.connections = server_state.connections
64
+ self.tasks = server_state.tasks
65
+ self.default_headers = server_state.default_headers
66
+
67
+ # Connection state
68
+ self.transport: asyncio.Transport = None # type: ignore[assignment]
69
+ self.server: tuple[str, int] | None = None
70
+ self.client: tuple[str, int] | None = None
71
+ self.scheme: Literal["wss", "ws"] = None # type: ignore[assignment]
72
+
73
+ # WebSocket state
74
+ self.queue: asyncio.Queue[ASGIReceiveEvent] = asyncio.Queue()
75
+ self.handshake_initiated = False
76
+ self.handshake_complete = False
77
+ self.close_sent = False
78
+ self.initial_response: tuple[int, list[tuple[str, str]], bytes] | None = None
79
+
80
+ extensions = []
81
+ if self.config.ws_per_message_deflate:
82
+ extensions = [
83
+ ServerPerMessageDeflateFactory(
84
+ server_max_window_bits=12,
85
+ client_max_window_bits=12,
86
+ compress_settings={"memLevel": 5},
87
+ )
88
+ ]
89
+ self.conn = ServerProtocol(
90
+ extensions=extensions,
91
+ max_size=self.config.ws_max_size,
92
+ logger=logging.getLogger("uvicorn.error"),
93
+ )
94
+
95
+ self.read_paused = False
96
+ self.writable = asyncio.Event()
97
+ self.writable.set()
98
+
99
+ # Buffers
100
+ self.bytes = b""
101
+
102
+ def connection_made(self, transport: BaseTransport) -> None:
103
+ """Called when a connection is made."""
104
+ transport = cast(Transport, transport)
105
+ self.connections.add(self)
106
+ self.transport = transport
107
+ self.server = get_local_addr(transport)
108
+ self.client = get_remote_addr(transport)
109
+ self.scheme = "wss" if is_ssl(transport) else "ws"
110
+
111
+ if self.logger.level <= TRACE_LOG_LEVEL:
112
+ prefix = "%s:%d - " % self.client if self.client else ""
113
+ self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection made", prefix)
114
+
115
+ def connection_lost(self, exc: Exception | None) -> None:
116
+ code = 1005 if self.handshake_complete else 1006
117
+ self.queue.put_nowait({"type": "websocket.disconnect", "code": code})
118
+ self.connections.remove(self)
119
+
120
+ if self.logger.level <= TRACE_LOG_LEVEL:
121
+ prefix = "%s:%d - " % self.client if self.client else ""
122
+ self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection lost", prefix)
123
+
124
+ self.handshake_complete = True
125
+ if exc is None:
126
+ self.transport.close()
127
+
128
+ def eof_received(self) -> None:
129
+ pass
130
+
131
+ def shutdown(self) -> None:
132
+ if self.handshake_complete:
133
+ self.queue.put_nowait({"type": "websocket.disconnect", "code": 1012})
134
+ self.conn.send_close(1012)
135
+ output = self.conn.data_to_send()
136
+ self.transport.write(b"".join(output))
137
+ else:
138
+ self.send_500_response()
139
+ self.transport.close()
140
+
141
+ def data_received(self, data: bytes) -> None:
142
+ self.conn.receive_data(data)
143
+ if self.conn.parser_exc is not None: # pragma: no cover
144
+ self.handle_parser_exception()
145
+ return
146
+ self.handle_events()
147
+
148
+ def handle_events(self) -> None:
149
+ for event in self.conn.events_received():
150
+ if isinstance(event, Request):
151
+ self.handle_connect(event)
152
+ if isinstance(event, Frame):
153
+ if event.opcode == Opcode.CONT:
154
+ self.handle_cont(event) # pragma: no cover
155
+ elif event.opcode == Opcode.TEXT:
156
+ self.handle_text(event)
157
+ elif event.opcode == Opcode.BINARY:
158
+ self.handle_bytes(event)
159
+ elif event.opcode == Opcode.PING:
160
+ self.handle_ping()
161
+ elif event.opcode == Opcode.PONG:
162
+ pass # pragma: no cover
163
+ elif event.opcode == Opcode.CLOSE:
164
+ self.handle_close(event)
165
+ else:
166
+ assert_never(event.opcode) # pragma: no cover
167
+
168
+ # Event handlers
169
+
170
+ def handle_connect(self, event: Request) -> None:
171
+ self.request = event
172
+ self.response = self.conn.accept(event)
173
+ self.handshake_initiated = True
174
+ if self.response.status_code != 101:
175
+ self.handshake_complete = True
176
+ self.close_sent = True
177
+ self.conn.send_response(self.response)
178
+ output = self.conn.data_to_send()
179
+ self.transport.write(b"".join(output))
180
+ self.transport.close()
181
+ return
182
+
183
+ headers = [
184
+ (key.encode("ascii"), value.encode("ascii", errors="surrogateescape"))
185
+ for key, value in event.headers.raw_items()
186
+ ]
187
+ raw_path, _, query_string = event.path.partition("?")
188
+ self.scope: WebSocketScope = {
189
+ "type": "websocket",
190
+ "asgi": {"version": self.config.asgi_version, "spec_version": "2.3"},
191
+ "http_version": "1.1",
192
+ "scheme": self.scheme,
193
+ "server": self.server,
194
+ "client": self.client,
195
+ "root_path": self.root_path,
196
+ "path": unquote(raw_path),
197
+ "raw_path": raw_path.encode("ascii"),
198
+ "query_string": query_string.encode("ascii"),
199
+ "headers": headers,
200
+ "subprotocols": event.headers.get_all("Sec-WebSocket-Protocol"),
201
+ "state": self.app_state.copy(),
202
+ "extensions": {"websocket.http.response": {}},
203
+ }
204
+ self.queue.put_nowait({"type": "websocket.connect"})
205
+ task = self.loop.create_task(self.run_asgi())
206
+ task.add_done_callback(self.on_task_complete)
207
+ self.tasks.add(task)
208
+
209
+ def handle_cont(self, event: Frame) -> None: # pragma: no cover
210
+ self.bytes += event.data
211
+ if event.fin:
212
+ self.send_receive_event_to_app()
213
+
214
+ def handle_text(self, event: Frame) -> None:
215
+ self.bytes = event.data
216
+ self.curr_msg_data_type: Literal["text", "bytes"] = "text"
217
+ if event.fin:
218
+ self.send_receive_event_to_app()
219
+
220
+ def handle_bytes(self, event: Frame) -> None:
221
+ self.bytes = event.data
222
+ self.curr_msg_data_type = "bytes"
223
+ if event.fin:
224
+ self.send_receive_event_to_app()
225
+
226
+ def send_receive_event_to_app(self) -> None:
227
+ if self.curr_msg_data_type == "text":
228
+ try:
229
+ self.queue.put_nowait({"type": "websocket.receive", "text": self.bytes.decode()})
230
+ except UnicodeDecodeError: # pragma: no cover
231
+ self.logger.exception("Invalid UTF-8 sequence received from client.")
232
+ self.conn.send_close(1007)
233
+ self.handle_parser_exception()
234
+ return
235
+ else:
236
+ self.queue.put_nowait({"type": "websocket.receive", "bytes": self.bytes})
237
+ if not self.read_paused:
238
+ self.read_paused = True
239
+ self.transport.pause_reading()
240
+
241
+ def handle_ping(self) -> None:
242
+ output = self.conn.data_to_send()
243
+ self.transport.write(b"".join(output))
244
+
245
+ def handle_close(self, event: Frame) -> None:
246
+ if not self.close_sent and not self.transport.is_closing():
247
+ assert self.conn.close_rcvd is not None
248
+ code = self.conn.close_rcvd.code
249
+ reason = self.conn.close_rcvd.reason
250
+ self.queue.put_nowait({"type": "websocket.disconnect", "code": code, "reason": reason})
251
+
252
+ output = self.conn.data_to_send()
253
+ self.transport.write(b"".join(output))
254
+ self.transport.close()
255
+
256
+ def handle_parser_exception(self) -> None: # pragma: no cover
257
+ assert self.conn.close_sent is not None
258
+ code = self.conn.close_sent.code
259
+ reason = self.conn.close_sent.reason
260
+ self.queue.put_nowait({"type": "websocket.disconnect", "code": code, "reason": reason})
261
+
262
+ output = self.conn.data_to_send()
263
+ self.transport.write(b"".join(output))
264
+ self.close_sent = True
265
+ self.transport.close()
266
+
267
+ def on_task_complete(self, task: asyncio.Task[None]) -> None:
268
+ self.tasks.discard(task)
269
+
270
+ async def run_asgi(self) -> None:
271
+ try:
272
+ result = await self.app(self.scope, self.receive, self.send)
273
+ except ClientDisconnected:
274
+ self.transport.close() # pragma: no cover
275
+ except BaseException:
276
+ self.logger.exception("Exception in ASGI application\n")
277
+ self.send_500_response()
278
+ self.transport.close()
279
+ else:
280
+ if not self.handshake_complete:
281
+ self.logger.error("ASGI callable returned without completing handshake.")
282
+ self.send_500_response()
283
+ self.transport.close()
284
+ elif result is not None:
285
+ self.logger.error("ASGI callable should return None, but returned '%s'.", result)
286
+ self.transport.close()
287
+
288
+ def send_500_response(self) -> None:
289
+ if self.initial_response or self.handshake_complete:
290
+ return
291
+ response = self.conn.reject(500, "Internal Server Error")
292
+ self.conn.send_response(response)
293
+ output = self.conn.data_to_send()
294
+ self.transport.write(b"".join(output))
295
+
296
+ async def send(self, message: ASGISendEvent) -> None:
297
+ await self.writable.wait()
298
+
299
+ message_type = message["type"]
300
+
301
+ if not self.handshake_complete and self.initial_response is None:
302
+ if message_type == "websocket.accept":
303
+ message = cast(WebSocketAcceptEvent, message)
304
+ self.logger.info(
305
+ '%s - "WebSocket %s" [accepted]',
306
+ self.scope["client"],
307
+ get_path_with_query_string(self.scope),
308
+ )
309
+ headers = [
310
+ (name.decode("latin-1").lower(), value.decode("latin-1").lower())
311
+ for name, value in (self.default_headers + list(message.get("headers", [])))
312
+ ]
313
+ accepted_subprotocol = message.get("subprotocol")
314
+ if accepted_subprotocol:
315
+ headers.append(("Sec-WebSocket-Protocol", accepted_subprotocol))
316
+ self.response.headers.update(headers)
317
+
318
+ if not self.transport.is_closing():
319
+ self.handshake_complete = True
320
+ self.conn.send_response(self.response)
321
+ output = self.conn.data_to_send()
322
+ self.transport.write(b"".join(output))
323
+
324
+ elif message_type == "websocket.close":
325
+ message = cast(WebSocketCloseEvent, message)
326
+ self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006})
327
+ self.logger.info(
328
+ '%s - "WebSocket %s" 403',
329
+ self.scope["client"],
330
+ get_path_with_query_string(self.scope),
331
+ )
332
+ response = self.conn.reject(HTTPStatus.FORBIDDEN, "")
333
+ self.conn.send_response(response)
334
+ output = self.conn.data_to_send()
335
+ self.close_sent = True
336
+ self.handshake_complete = True
337
+ self.transport.write(b"".join(output))
338
+ self.transport.close()
339
+ elif message_type == "websocket.http.response.start" and self.initial_response is None:
340
+ message = cast(WebSocketResponseStartEvent, message)
341
+ if not (100 <= message["status"] < 600):
342
+ raise RuntimeError("Invalid HTTP status code '%d' in response." % message["status"])
343
+ self.logger.info(
344
+ '%s - "WebSocket %s" %d',
345
+ self.scope["client"],
346
+ get_path_with_query_string(self.scope),
347
+ message["status"],
348
+ )
349
+ headers = [
350
+ (name.decode("latin-1"), value.decode("latin-1"))
351
+ for name, value in list(message.get("headers", []))
352
+ ]
353
+ self.initial_response = (message["status"], headers, b"")
354
+ else:
355
+ msg = (
356
+ "Expected ASGI message 'websocket.accept', 'websocket.close' "
357
+ "or 'websocket.http.response.start' "
358
+ "but got '%s'."
359
+ )
360
+ raise RuntimeError(msg % message_type)
361
+
362
+ elif not self.close_sent and self.initial_response is None:
363
+ try:
364
+ if message_type == "websocket.send":
365
+ message = cast(WebSocketSendEvent, message)
366
+ bytes_data = message.get("bytes")
367
+ text_data = message.get("text")
368
+ if text_data:
369
+ self.conn.send_text(text_data.encode())
370
+ elif bytes_data:
371
+ self.conn.send_binary(bytes_data)
372
+ output = self.conn.data_to_send()
373
+ self.transport.write(b"".join(output))
374
+
375
+ elif message_type == "websocket.close" and not self.transport.is_closing():
376
+ message = cast(WebSocketCloseEvent, message)
377
+ code = message.get("code", 1000)
378
+ reason = message.get("reason", "") or ""
379
+ self.queue.put_nowait({"type": "websocket.disconnect", "code": code, "reason": reason})
380
+ self.conn.send_close(code, reason)
381
+ output = self.conn.data_to_send()
382
+ self.transport.write(b"".join(output))
383
+ self.close_sent = True
384
+ self.transport.close()
385
+ else:
386
+ msg = "Expected ASGI message 'websocket.send' or 'websocket.close', but got '%s'."
387
+ raise RuntimeError(msg % message_type)
388
+ except InvalidState:
389
+ raise ClientDisconnected()
390
+ elif self.initial_response is not None:
391
+ if message_type == "websocket.http.response.body":
392
+ message = cast(WebSocketResponseBodyEvent, message)
393
+ body = self.initial_response[2] + message["body"]
394
+ self.initial_response = self.initial_response[:2] + (body,)
395
+ if not message.get("more_body", False):
396
+ response = self.conn.reject(self.initial_response[0], body.decode())
397
+ response.headers.update(self.initial_response[1])
398
+ self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006})
399
+ self.conn.send_response(response)
400
+ output = self.conn.data_to_send()
401
+ self.close_sent = True
402
+ self.transport.write(b"".join(output))
403
+ self.transport.close()
404
+ else: # pragma: no cover
405
+ msg = "Expected ASGI message 'websocket.http.response.body' but got '%s'."
406
+ raise RuntimeError(msg % message_type)
407
+
408
+ else:
409
+ msg = "Unexpected ASGI message '%s', after sending 'websocket.close'."
410
+ raise RuntimeError(msg % message_type)
411
+
412
+ async def receive(self) -> ASGIReceiveEvent:
413
+ message = await self.queue.get()
414
+ if self.read_paused and self.queue.empty():
415
+ self.read_paused = False
416
+ self.transport.resume_reading()
417
+ return message
@@ -2,8 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import logging
5
- import typing
6
- from typing import Literal, cast
5
+ from typing import Any, Literal, cast
7
6
  from urllib.parse import unquote
8
7
 
9
8
  import wsproto
@@ -27,6 +26,7 @@ from uvicorn.config import Config
27
26
  from uvicorn.logging import TRACE_LOG_LEVEL
28
27
  from uvicorn.protocols.utils import (
29
28
  ClientDisconnected,
29
+ get_client_addr,
30
30
  get_local_addr,
31
31
  get_path_with_query_string,
32
32
  get_remote_addr,
@@ -40,7 +40,7 @@ class WSProtocol(asyncio.Protocol):
40
40
  self,
41
41
  config: Config,
42
42
  server_state: ServerState,
43
- app_state: dict[str, typing.Any],
43
+ app_state: dict[str, Any],
44
44
  _loop: asyncio.AbstractEventLoop | None = None,
45
45
  ) -> None:
46
46
  if not config.loaded:
@@ -255,10 +255,10 @@ class WSProtocol(asyncio.Protocol):
255
255
 
256
256
  if not self.handshake_complete:
257
257
  if message_type == "websocket.accept":
258
- message = typing.cast(WebSocketAcceptEvent, message)
258
+ message = cast(WebSocketAcceptEvent, message)
259
259
  self.logger.info(
260
260
  '%s - "WebSocket %s" [accepted]',
261
- self.scope["client"],
261
+ get_client_addr(self.scope),
262
262
  get_path_with_query_string(self.scope),
263
263
  )
264
264
  subprotocol = message.get("subprotocol")
@@ -281,7 +281,7 @@ class WSProtocol(asyncio.Protocol):
281
281
  self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006})
282
282
  self.logger.info(
283
283
  '%s - "WebSocket %s" 403',
284
- self.scope["client"],
284
+ get_client_addr(self.scope),
285
285
  get_path_with_query_string(self.scope),
286
286
  )
287
287
  self.handshake_complete = True
@@ -292,14 +292,14 @@ class WSProtocol(asyncio.Protocol):
292
292
  self.transport.close()
293
293
 
294
294
  elif message_type == "websocket.http.response.start":
295
- message = typing.cast(WebSocketResponseStartEvent, message)
295
+ message = cast(WebSocketResponseStartEvent, message)
296
296
  # ensure status code is in the valid range
297
297
  if not (100 <= message["status"] < 600):
298
298
  msg = "Invalid HTTP status code '%d' in response."
299
299
  raise RuntimeError(msg % message["status"])
300
300
  self.logger.info(
301
301
  '%s - "WebSocket %s" %d',
302
- self.scope["client"],
302
+ get_client_addr(self.scope),
303
303
  get_path_with_query_string(self.scope),
304
304
  message["status"],
305
305
  )
@@ -324,7 +324,7 @@ class WSProtocol(asyncio.Protocol):
324
324
  elif not self.close_sent and not self.response_started:
325
325
  try:
326
326
  if message_type == "websocket.send":
327
- message = typing.cast(WebSocketSendEvent, message)
327
+ message = cast(WebSocketSendEvent, message)
328
328
  bytes_data = message.get("bytes")
329
329
  text_data = message.get("text")
330
330
  data = text_data if bytes_data is None else bytes_data
@@ -333,7 +333,7 @@ class WSProtocol(asyncio.Protocol):
333
333
  self.transport.write(output)
334
334
 
335
335
  elif message_type == "websocket.close":
336
- message = typing.cast(WebSocketCloseEvent, message)
336
+ message = cast(WebSocketCloseEvent, message)
337
337
  self.close_sent = True
338
338
  code = message.get("code", 1000)
339
339
  reason = message.get("reason", "") or ""
@@ -350,7 +350,7 @@ class WSProtocol(asyncio.Protocol):
350
350
  raise ClientDisconnected from exc
351
351
  elif self.response_started:
352
352
  if message_type == "websocket.http.response.body":
353
- message = typing.cast("WebSocketResponseBodyEvent", message)
353
+ message = cast("WebSocketResponseBodyEvent", message)
354
354
  body_finished = not message.get("more_body", False)
355
355
  reject_data = events.RejectData(data=message["body"], body_finished=body_finished)
356
356
  output = self.conn.send(reject_data)
@@ -23,9 +23,10 @@ if TYPE_CHECKING:
23
23
  from uvicorn.protocols.http.h11_impl import H11Protocol
24
24
  from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
25
25
  from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol
26
+ from uvicorn.protocols.websockets.websockets_sansio_impl import WebSocketsSansIOProtocol
26
27
  from uvicorn.protocols.websockets.wsproto_impl import WSProtocol
27
28
 
28
- Protocols = Union[H11Protocol, HttpToolsProtocol, WSProtocol, WebSocketProtocol]
29
+ Protocols = Union[H11Protocol, HttpToolsProtocol, WSProtocol, WebSocketProtocol, WebSocketsSansIOProtocol]
29
30
 
30
31
  HANDLED_SIGNALS = (
31
32
  signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
@@ -63,10 +63,7 @@ class WatchFilesReload(BaseReload):
63
63
  self.reloader_name = "WatchFiles"
64
64
  self.reload_dirs = []
65
65
  for directory in config.reload_dirs:
66
- if Path.cwd() not in directory.parents:
67
- self.reload_dirs.append(directory)
68
- if Path.cwd() not in self.reload_dirs:
69
- self.reload_dirs.append(Path.cwd())
66
+ self.reload_dirs.append(directory)
70
67
 
71
68
  self.watch_filter = FileFilter(config)
72
69
  self.watcher = watch(
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes