uvicorn 0.28.1__tar.gz → 0.30.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 (46) hide show
  1. {uvicorn-0.28.1 → uvicorn-0.30.0}/PKG-INFO +1 -1
  2. {uvicorn-0.28.1 → uvicorn-0.30.0}/pyproject.toml +7 -1
  3. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/__init__.py +1 -1
  4. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/_subprocess.py +8 -2
  5. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/_types.py +3 -4
  6. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/config.py +5 -4
  7. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/main.py +16 -13
  8. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/middleware/proxy_headers.py +1 -0
  9. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/protocols/http/flow_control.py +14 -24
  10. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/protocols/http/h11_impl.py +2 -13
  11. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/protocols/http/httptools_impl.py +16 -31
  12. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/protocols/utils.py +1 -2
  13. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/protocols/websockets/websockets_impl.py +17 -16
  14. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/protocols/websockets/wsproto_impl.py +8 -9
  15. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/server.py +25 -14
  16. uvicorn-0.30.0/uvicorn/supervisors/multiprocess.py +223 -0
  17. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/workers.py +7 -0
  18. uvicorn-0.28.1/uvicorn/supervisors/multiprocess.py +0 -70
  19. {uvicorn-0.28.1 → uvicorn-0.30.0}/.gitignore +0 -0
  20. {uvicorn-0.28.1 → uvicorn-0.30.0}/LICENSE.md +0 -0
  21. {uvicorn-0.28.1 → uvicorn-0.30.0}/README.md +0 -0
  22. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/__main__.py +0 -0
  23. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/importer.py +0 -0
  24. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/lifespan/__init__.py +0 -0
  25. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/lifespan/off.py +0 -0
  26. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/lifespan/on.py +0 -0
  27. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/logging.py +0 -0
  28. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/loops/__init__.py +0 -0
  29. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/loops/asyncio.py +0 -0
  30. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/loops/auto.py +0 -0
  31. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/loops/uvloop.py +0 -0
  32. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/middleware/__init__.py +0 -0
  33. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/middleware/asgi2.py +0 -0
  34. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/middleware/message_logger.py +0 -0
  35. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/middleware/wsgi.py +0 -0
  36. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/protocols/__init__.py +0 -0
  37. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/protocols/http/__init__.py +0 -0
  38. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/protocols/http/auto.py +0 -0
  39. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/protocols/websockets/__init__.py +0 -0
  40. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/protocols/websockets/auto.py +0 -0
  41. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/py.typed +0 -0
  42. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/supervisors/__init__.py +0 -0
  43. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/supervisors/basereload.py +0 -0
  44. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/supervisors/statreload.py +0 -0
  45. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/supervisors/watchfilesreload.py +0 -0
  46. {uvicorn-0.28.1 → uvicorn-0.30.0}/uvicorn/supervisors/watchgodreload.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: uvicorn
3
- Version: 0.28.1
3
+ Version: 0.30.0
4
4
  Summary: The lightning-fast ASGI server.
5
5
  Project-URL: Changelog, https://github.com/encode/uvicorn/blob/master/CHANGELOG.md
6
6
  Project-URL: Funding, https://github.com/sponsors/encode
@@ -62,6 +62,8 @@ include = ["/uvicorn"]
62
62
 
63
63
  [tool.ruff]
64
64
  line-length = 120
65
+
66
+ [tool.ruff.lint]
65
67
  select = ["E", "F", "I", "FA", "UP"]
66
68
  ignore = ["B904", "B028"]
67
69
 
@@ -113,7 +115,11 @@ exclude_lines = [
113
115
  ]
114
116
 
115
117
  [tool.coverage.coverage_conditional_plugin.omit]
116
- "sys_platform == 'win32'" = ["uvicorn/loops/uvloop.py"]
118
+ "sys_platform == 'win32'" = [
119
+ "uvicorn/loops/uvloop.py",
120
+ "uvicorn/supervisors/multiprocess.py",
121
+ "tests/supervisors/test_multiprocess.py",
122
+ ]
117
123
  "sys_platform != 'win32'" = ["uvicorn/loops/asyncio.py"]
118
124
 
119
125
  [tool.coverage.coverage_conditional_plugin.rules]
@@ -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.28.1"
4
+ __version__ = "0.30.0"
5
5
  __all__ = ["main", "run", "Config", "Server"]
@@ -2,6 +2,7 @@
2
2
  Some light wrappers around Python's multiprocessing, to deal with cleanly
3
3
  starting child processes.
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
8
  import multiprocessing
@@ -74,5 +75,10 @@ def subprocess_started(
74
75
  # Logging needs to be setup again for each child.
75
76
  config.configure_logging()
76
77
 
77
- # Now we can call into `Server.run(sockets=sockets)`
78
- target(sockets=sockets)
78
+ try:
79
+ # Now we can call into `Server.run(sockets=sockets)`
80
+ target(sockets=sockets)
81
+ except KeyboardInterrupt: # pragma: no cover
82
+ # supress the exception to avoid a traceback from subprocess.Popen
83
+ # the parent already expects us to end, so no vital information is lost
84
+ pass
@@ -27,6 +27,7 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
27
  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28
28
  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
29
  """
30
+
30
31
  from __future__ import annotations
31
32
 
32
33
  import sys
@@ -274,11 +275,9 @@ ASGISendCallable = Callable[[ASGISendEvent], Awaitable[None]]
274
275
 
275
276
 
276
277
  class ASGI2Protocol(Protocol):
277
- def __init__(self, scope: Scope) -> None:
278
- ... # pragma: no cover
278
+ def __init__(self, scope: Scope) -> None: ... # pragma: no cover
279
279
 
280
- async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
281
- ... # pragma: no cover
280
+ async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... # pragma: no cover
282
281
 
283
282
 
284
283
  ASGI2Application = Type[ASGI2Protocol]
@@ -9,8 +9,9 @@ import os
9
9
  import socket
10
10
  import ssl
11
11
  import sys
12
+ from configparser import RawConfigParser
12
13
  from pathlib import Path
13
- from typing import Any, Awaitable, Callable, Literal
14
+ from typing import IO, Any, Awaitable, Callable, Literal
14
15
 
15
16
  import click
16
17
 
@@ -189,7 +190,7 @@ class Config:
189
190
  ws_per_message_deflate: bool = True,
190
191
  lifespan: LifespanType = "auto",
191
192
  env_file: str | os.PathLike[str] | None = None,
192
- log_config: dict[str, Any] | str | None = LOGGING_CONFIG,
193
+ log_config: dict[str, Any] | str | RawConfigParser | IO[Any] | None = LOGGING_CONFIG,
193
194
  log_level: str | int | None = None,
194
195
  access_log: bool = True,
195
196
  use_colors: bool | None = None,
@@ -362,11 +363,11 @@ class Config:
362
363
  self.log_config["formatters"]["default"]["use_colors"] = self.use_colors
363
364
  self.log_config["formatters"]["access"]["use_colors"] = self.use_colors
364
365
  logging.config.dictConfig(self.log_config)
365
- elif self.log_config.endswith(".json"):
366
+ elif isinstance(self.log_config, str) and self.log_config.endswith(".json"):
366
367
  with open(self.log_config) as file:
367
368
  loaded_config = json.load(file)
368
369
  logging.config.dictConfig(loaded_config)
369
- elif self.log_config.endswith((".yaml", ".yml")):
370
+ elif isinstance(self.log_config, str) and self.log_config.endswith((".yaml", ".yml")):
370
371
  # Install the PyYAML package or the uvicorn[standard] optional
371
372
  # dependencies to enable this functionality.
372
373
  import yaml
@@ -6,7 +6,8 @@ import os
6
6
  import platform
7
7
  import ssl
8
8
  import sys
9
- from typing import Any, Callable
9
+ from configparser import RawConfigParser
10
+ from typing import IO, Any, Callable
10
11
 
11
12
  import click
12
13
 
@@ -47,7 +48,7 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
47
48
  if not value or ctx.resilient_parsing:
48
49
  return
49
50
  click.echo(
50
- "Running uvicorn {version} with {py_implementation} {py_version} on {system}".format(
51
+ "Running uvicorn {version} with {py_implementation} {py_version} on {system}".format( # noqa: UP032
51
52
  version=uvicorn.__version__,
52
53
  py_implementation=platform.python_implementation(),
53
54
  py_version=platform.python_version(),
@@ -481,7 +482,7 @@ def run(
481
482
  reload_delay: float = 0.25,
482
483
  workers: int | None = None,
483
484
  env_file: str | os.PathLike[str] | None = None,
484
- log_config: dict[str, Any] | str | None = LOGGING_CONFIG,
485
+ log_config: dict[str, Any] | str | RawConfigParser | IO[Any] | None = LOGGING_CONFIG,
485
486
  log_level: str | int | None = None,
486
487
  access_log: bool = True,
487
488
  proxy_headers: bool = True,
@@ -565,16 +566,18 @@ def run(
565
566
  logger.warning("You must pass the application as an import string to enable 'reload' or " "'workers'.")
566
567
  sys.exit(1)
567
568
 
568
- if config.should_reload:
569
- sock = config.bind_socket()
570
- ChangeReload(config, target=server.run, sockets=[sock]).run()
571
- elif config.workers > 1:
572
- sock = config.bind_socket()
573
- Multiprocess(config, target=server.run, sockets=[sock]).run()
574
- else:
575
- server.run()
576
- if config.uds and os.path.exists(config.uds):
577
- os.remove(config.uds) # pragma: py-win32
569
+ try:
570
+ if config.should_reload:
571
+ sock = config.bind_socket()
572
+ ChangeReload(config, target=server.run, sockets=[sock]).run()
573
+ elif config.workers > 1:
574
+ sock = config.bind_socket()
575
+ Multiprocess(config, target=server.run, sockets=[sock]).run()
576
+ else:
577
+ server.run()
578
+ finally:
579
+ if config.uds and os.path.exists(config.uds):
580
+ os.remove(config.uds) # pragma: py-win32
578
581
 
579
582
  if not server.started and not config.should_reload and config.workers == 1:
580
583
  sys.exit(STARTUP_FAILURE)
@@ -8,6 +8,7 @@ the connecting client, rather that the connecting proxy.
8
8
 
9
9
  https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#Proxies
10
10
  """
11
+
11
12
  from __future__ import annotations
12
13
 
13
14
  from typing import Union, cast
@@ -1,12 +1,6 @@
1
1
  import asyncio
2
2
 
3
- from uvicorn._types import (
4
- ASGIReceiveCallable,
5
- ASGISendCallable,
6
- HTTPResponseBodyEvent,
7
- HTTPResponseStartEvent,
8
- Scope,
9
- )
3
+ from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope
10
4
 
11
5
  CLOSE_HEADER = (b"connection", b"close")
12
6
 
@@ -45,20 +39,16 @@ class FlowControl:
45
39
  self._is_writable_event.set()
46
40
 
47
41
 
48
- async def service_unavailable(scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable") -> None:
49
- response_start: "HTTPResponseStartEvent" = {
50
- "type": "http.response.start",
51
- "status": 503,
52
- "headers": [
53
- (b"content-type", b"text/plain; charset=utf-8"),
54
- (b"connection", b"close"),
55
- ],
56
- }
57
- await send(response_start)
58
-
59
- response_body: "HTTPResponseBodyEvent" = {
60
- "type": "http.response.body",
61
- "body": b"Service Unavailable",
62
- "more_body": False,
63
- }
64
- await send(response_body)
42
+ async def service_unavailable(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
43
+ await send(
44
+ {
45
+ "type": "http.response.start",
46
+ "status": 503,
47
+ "headers": [
48
+ (b"content-type", b"text/plain; charset=utf-8"),
49
+ (b"content-length", b"19"),
50
+ (b"connection", b"close"),
51
+ ],
52
+ }
53
+ )
54
+ await send({"type": "http.response.body", "body": b"Service Unavailable", "more_body": False})
@@ -20,19 +20,8 @@ from uvicorn._types import (
20
20
  )
21
21
  from uvicorn.config import Config
22
22
  from uvicorn.logging import TRACE_LOG_LEVEL
23
- from uvicorn.protocols.http.flow_control import (
24
- CLOSE_HEADER,
25
- HIGH_WATER_LIMIT,
26
- FlowControl,
27
- service_unavailable,
28
- )
29
- from uvicorn.protocols.utils import (
30
- get_client_addr,
31
- get_local_addr,
32
- get_path_with_query_string,
33
- get_remote_addr,
34
- is_ssl,
35
- )
23
+ from uvicorn.protocols.http.flow_control import CLOSE_HEADER, HIGH_WATER_LIMIT, FlowControl, service_unavailable
24
+ from uvicorn.protocols.utils import get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, is_ssl
36
25
  from uvicorn.server import ServerState
37
26
 
38
27
 
@@ -16,29 +16,17 @@ from uvicorn._types import (
16
16
  ASGIReceiveEvent,
17
17
  ASGISendEvent,
18
18
  HTTPRequestEvent,
19
- HTTPResponseBodyEvent,
20
19
  HTTPResponseStartEvent,
21
20
  HTTPScope,
22
21
  )
23
22
  from uvicorn.config import Config
24
23
  from uvicorn.logging import TRACE_LOG_LEVEL
25
- from uvicorn.protocols.http.flow_control import (
26
- CLOSE_HEADER,
27
- HIGH_WATER_LIMIT,
28
- FlowControl,
29
- service_unavailable,
30
- )
31
- from uvicorn.protocols.utils import (
32
- get_client_addr,
33
- get_local_addr,
34
- get_path_with_query_string,
35
- get_remote_addr,
36
- is_ssl,
37
- )
24
+ from uvicorn.protocols.http.flow_control import CLOSE_HEADER, HIGH_WATER_LIMIT, FlowControl, service_unavailable
25
+ from uvicorn.protocols.utils import get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, is_ssl
38
26
  from uvicorn.server import ServerState
39
27
 
40
- HEADER_RE = re.compile(b'[\x00-\x1F\x7F()<>@,;:[]={} \t\\"]')
41
- HEADER_VALUE_RE = re.compile(b"[\x00-\x1F\x7F]")
28
+ HEADER_RE = re.compile(b'[\x00-\x1f\x7f()<>@,;:[]={} \t\\"]')
29
+ HEADER_VALUE_RE = re.compile(b"[\x00-\x1f\x7f]")
42
30
 
43
31
 
44
32
  def _get_status_line(status_code: int) -> bytes:
@@ -435,21 +423,18 @@ class RequestResponseCycle:
435
423
  self.on_response = lambda: None
436
424
 
437
425
  async def send_500_response(self) -> None:
438
- response_start_event: HTTPResponseStartEvent = {
439
- "type": "http.response.start",
440
- "status": 500,
441
- "headers": [
442
- (b"content-type", b"text/plain; charset=utf-8"),
443
- (b"connection", b"close"),
444
- ],
445
- }
446
- await self.send(response_start_event)
447
- response_body_event: HTTPResponseBodyEvent = {
448
- "type": "http.response.body",
449
- "body": b"Internal Server Error",
450
- "more_body": False,
451
- }
452
- await self.send(response_body_event)
426
+ await self.send(
427
+ {
428
+ "type": "http.response.start",
429
+ "status": 500,
430
+ "headers": [
431
+ (b"content-type", b"text/plain; charset=utf-8"),
432
+ (b"content-length", b"21"),
433
+ (b"connection", b"close"),
434
+ ],
435
+ }
436
+ )
437
+ await self.send({"type": "http.response.body", "body": b"Internal Server Error", "more_body": False})
453
438
 
454
439
  # ASGI interface
455
440
  async def send(self, message: ASGISendEvent) -> None:
@@ -6,8 +6,7 @@ import urllib.parse
6
6
  from uvicorn._types import WWWScope
7
7
 
8
8
 
9
- class ClientDisconnected(IOError):
10
- ...
9
+ class ClientDisconnected(IOError): ...
11
10
 
12
11
 
13
12
  def get_remote_addr(transport: asyncio.Transport) -> tuple[str, int] | None:
@@ -7,14 +7,17 @@ from typing import Any, Literal, Optional, Sequence, cast
7
7
  from urllib.parse import unquote
8
8
 
9
9
  import websockets
10
+ import websockets.legacy.handshake
10
11
  from websockets.datastructures import Headers
11
12
  from websockets.exceptions import ConnectionClosed
13
+ from websockets.extensions.base import ServerExtensionFactory
12
14
  from websockets.extensions.permessage_deflate import ServerPerMessageDeflateFactory
13
15
  from websockets.legacy.server import HTTPResponse
14
16
  from websockets.server import WebSocketServerProtocol
15
17
  from websockets.typing import Subprotocol
16
18
 
17
19
  from uvicorn._types import (
20
+ ASGI3Application,
18
21
  ASGISendEvent,
19
22
  WebSocketAcceptEvent,
20
23
  WebSocketCloseEvent,
@@ -53,6 +56,7 @@ class Server:
53
56
 
54
57
  class WebSocketProtocol(WebSocketServerProtocol):
55
58
  extra_headers: list[tuple[str, str]]
59
+ logger: logging.Logger | logging.LoggerAdapter[Any]
56
60
 
57
61
  def __init__(
58
62
  self,
@@ -65,7 +69,7 @@ class WebSocketProtocol(WebSocketServerProtocol):
65
69
  config.load()
66
70
 
67
71
  self.config = config
68
- self.app = config.loaded_app
72
+ self.app = cast(ASGI3Application, config.loaded_app)
69
73
  self.loop = _loop or asyncio.get_event_loop()
70
74
  self.root_path = config.root_path
71
75
  self.app_state = app_state
@@ -92,7 +96,7 @@ class WebSocketProtocol(WebSocketServerProtocol):
92
96
 
93
97
  self.ws_server: Server = Server() # type: ignore[assignment]
94
98
 
95
- extensions = []
99
+ extensions: list[ServerExtensionFactory] = []
96
100
  if self.config.ws_per_message_deflate:
97
101
  extensions.append(ServerPerMessageDeflateFactory())
98
102
 
@@ -147,10 +151,10 @@ class WebSocketProtocol(WebSocketServerProtocol):
147
151
  self.send_500_response()
148
152
  self.transport.close()
149
153
 
150
- def on_task_complete(self, task: asyncio.Task) -> None:
154
+ def on_task_complete(self, task: asyncio.Task[None]) -> None:
151
155
  self.tasks.discard(task)
152
156
 
153
- async def process_request(self, path: str, headers: Headers) -> HTTPResponse | None:
157
+ async def process_request(self, path: str, request_headers: Headers) -> HTTPResponse | None:
154
158
  """
155
159
  This hook is called to determine if the websocket should return
156
160
  an HTTP response and close.
@@ -161,15 +165,15 @@ class WebSocketProtocol(WebSocketServerProtocol):
161
165
  """
162
166
  path_portion, _, query_string = path.partition("?")
163
167
 
164
- websockets.legacy.handshake.check_request(headers)
168
+ websockets.legacy.handshake.check_request(request_headers)
165
169
 
166
- subprotocols = []
167
- for header in headers.get_all("Sec-WebSocket-Protocol"):
170
+ subprotocols: list[str] = []
171
+ for header in request_headers.get_all("Sec-WebSocket-Protocol"):
168
172
  subprotocols.extend([token.strip() for token in header.split(",")])
169
173
 
170
174
  asgi_headers = [
171
175
  (name.encode("ascii"), value.encode("ascii", errors="surrogateescape"))
172
- for name, value in headers.raw_items()
176
+ for name, value in request_headers.raw_items()
173
177
  ]
174
178
  path = unquote(path_portion)
175
179
  full_path = self.root_path + path
@@ -237,14 +241,13 @@ class WebSocketProtocol(WebSocketServerProtocol):
237
241
  termination states.
238
242
  """
239
243
  try:
240
- result = await self.app(self.scope, self.asgi_receive, self.asgi_send)
244
+ result = await self.app(self.scope, self.asgi_receive, self.asgi_send) # type: ignore[func-returns-value]
241
245
  except ClientDisconnected:
242
246
  self.closed_event.set()
243
247
  self.transport.close()
244
- except BaseException as exc:
248
+ except BaseException:
245
249
  self.closed_event.set()
246
- msg = "Exception in ASGI application\n"
247
- self.logger.error(msg, exc_info=exc)
250
+ self.logger.exception("Exception in ASGI application\n")
248
251
  if not self.handshake_started_event.is_set():
249
252
  self.send_500_response()
250
253
  else:
@@ -253,13 +256,11 @@ class WebSocketProtocol(WebSocketServerProtocol):
253
256
  else:
254
257
  self.closed_event.set()
255
258
  if not self.handshake_started_event.is_set():
256
- msg = "ASGI callable returned without sending handshake."
257
- self.logger.error(msg)
259
+ self.logger.error("ASGI callable returned without sending handshake.")
258
260
  self.send_500_response()
259
261
  self.transport.close()
260
262
  elif result is not None:
261
- msg = "ASGI callable should return None, but returned '%s'."
262
- self.logger.error(msg, result)
263
+ self.logger.error("ASGI callable should return None, but returned '%s'.", result)
263
264
  await self.handshake_completed_event.wait()
264
265
  self.transport.close()
265
266
 
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import logging
5
5
  import typing
6
- from typing import Literal
6
+ from typing import Literal, cast
7
7
  from urllib.parse import unquote
8
8
 
9
9
  import wsproto
@@ -13,6 +13,7 @@ from wsproto.extensions import Extension, PerMessageDeflate
13
13
  from wsproto.utilities import LocalProtocolError, RemoteProtocolError
14
14
 
15
15
  from uvicorn._types import (
16
+ ASGI3Application,
16
17
  ASGISendEvent,
17
18
  WebSocketAcceptEvent,
18
19
  WebSocketCloseEvent,
@@ -46,7 +47,7 @@ class WSProtocol(asyncio.Protocol):
46
47
  config.load()
47
48
 
48
49
  self.config = config
49
- self.app = config.loaded_app
50
+ self.app = cast(ASGI3Application, config.loaded_app)
50
51
  self.loop = _loop or asyncio.get_event_loop()
51
52
  self.logger = logging.getLogger("uvicorn.error")
52
53
  self.root_path = config.root_path
@@ -156,7 +157,7 @@ class WSProtocol(asyncio.Protocol):
156
157
  self.send_500_response()
157
158
  self.transport.close()
158
159
 
159
- def on_task_complete(self, task: asyncio.Task) -> None:
160
+ def on_task_complete(self, task: asyncio.Task[None]) -> None:
160
161
  self.tasks.discard(task)
161
162
 
162
163
  # Event handlers
@@ -220,7 +221,7 @@ class WSProtocol(asyncio.Protocol):
220
221
  def send_500_response(self) -> None:
221
222
  if self.response_started or self.handshake_complete:
222
223
  return # we cannot send responses anymore
223
- headers = [
224
+ headers: list[tuple[bytes, bytes]] = [
224
225
  (b"content-type", b"text/plain; charset=utf-8"),
225
226
  (b"connection", b"close"),
226
227
  ]
@@ -230,7 +231,7 @@ class WSProtocol(asyncio.Protocol):
230
231
 
231
232
  async def run_asgi(self) -> None:
232
233
  try:
233
- result = await self.app(self.scope, self.receive, self.send)
234
+ result = await self.app(self.scope, self.receive, self.send) # type: ignore[func-returns-value]
234
235
  except ClientDisconnected:
235
236
  self.transport.close()
236
237
  except BaseException:
@@ -239,13 +240,11 @@ class WSProtocol(asyncio.Protocol):
239
240
  self.transport.close()
240
241
  else:
241
242
  if not self.handshake_complete:
242
- msg = "ASGI callable returned without completing handshake."
243
- self.logger.error(msg)
243
+ self.logger.error("ASGI callable returned without completing handshake.")
244
244
  self.send_500_response()
245
245
  self.transport.close()
246
246
  elif result is not None:
247
- msg = "ASGI callable should return None, but returned '%s'."
248
- self.logger.error(msg, result)
247
+ self.logger.error("ASGI callable should return None, but returned '%s'.", result)
249
248
  self.transport.close()
250
249
 
251
250
  async def send(self, message: ASGISendEvent) -> None:
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import contextlib
4
5
  import logging
5
6
  import os
6
7
  import platform
@@ -11,7 +12,7 @@ import threading
11
12
  import time
12
13
  from email.utils import formatdate
13
14
  from types import FrameType
14
- from typing import TYPE_CHECKING, Sequence, Union
15
+ from typing import TYPE_CHECKING, Generator, Sequence, Union
15
16
 
16
17
  import click
17
18
 
@@ -57,11 +58,17 @@ class Server:
57
58
  self.force_exit = False
58
59
  self.last_notified = 0.0
59
60
 
61
+ self._captured_signals: list[int] = []
62
+
60
63
  def run(self, sockets: list[socket.socket] | None = None) -> None:
61
64
  self.config.setup_event_loop()
62
65
  return asyncio.run(self.serve(sockets=sockets))
63
66
 
64
67
  async def serve(self, sockets: list[socket.socket] | None = None) -> None:
68
+ with self.capture_signals():
69
+ await self._serve(sockets)
70
+
71
+ async def _serve(self, sockets: list[socket.socket] | None = None) -> None:
65
72
  process_id = os.getpid()
66
73
 
67
74
  config = self.config
@@ -70,8 +77,6 @@ class Server:
70
77
 
71
78
  self.lifespan = config.lifespan_class(config)
72
79
 
73
- self.install_signal_handlers()
74
-
75
80
  message = "Started server process [%d]"
76
81
  color_message = "Started server process [" + click.style("%d", fg="cyan") + "]"
77
82
  logger.info(message, process_id, extra={"color_message": color_message})
@@ -302,22 +307,28 @@ class Server:
302
307
  for server in self.servers:
303
308
  await server.wait_closed()
304
309
 
305
- def install_signal_handlers(self) -> None:
310
+ @contextlib.contextmanager
311
+ def capture_signals(self) -> Generator[None, None, None]:
312
+ # Signals can only be listened to from the main thread.
306
313
  if threading.current_thread() is not threading.main_thread():
307
- # Signals can only be listened to from the main thread.
314
+ yield
308
315
  return
309
-
310
- loop = asyncio.get_event_loop()
311
-
316
+ # always use signal.signal, even if loop.add_signal_handler is available
317
+ # this allows to restore previous signal handlers later on
318
+ original_handlers = {sig: signal.signal(sig, self.handle_exit) for sig in HANDLED_SIGNALS}
312
319
  try:
313
- for sig in HANDLED_SIGNALS:
314
- loop.add_signal_handler(sig, self.handle_exit, sig, None)
315
- except NotImplementedError: # pragma: no cover
316
- # Windows
317
- for sig in HANDLED_SIGNALS:
318
- signal.signal(sig, self.handle_exit)
320
+ yield
321
+ finally:
322
+ for sig, handler in original_handlers.items():
323
+ signal.signal(sig, handler)
324
+ # If we did gracefully shut down due to a signal, try to
325
+ # trigger the expected behaviour now; multiple signals would be
326
+ # done LIFO, see https://stackoverflow.com/questions/48434964
327
+ for captured_signal in reversed(self._captured_signals):
328
+ signal.raise_signal(captured_signal)
319
329
 
320
330
  def handle_exit(self, sig: int, frame: FrameType | None) -> None:
331
+ self._captured_signals.append(sig)
321
332
  if self.should_exit and sig == signal.SIGINT:
322
333
  self.force_exit = True
323
334
  else:
@@ -0,0 +1,223 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import signal
6
+ import threading
7
+ from multiprocessing import Pipe
8
+ from socket import socket
9
+ from typing import Any, Callable
10
+
11
+ import click
12
+
13
+ from uvicorn._subprocess import get_subprocess
14
+ from uvicorn.config import Config
15
+
16
+ SIGNALS = {
17
+ getattr(signal, f"SIG{x}"): x
18
+ for x in "INT TERM BREAK HUP QUIT TTIN TTOU USR1 USR2 WINCH".split()
19
+ if hasattr(signal, f"SIG{x}")
20
+ }
21
+
22
+ logger = logging.getLogger("uvicorn.error")
23
+
24
+
25
+ class Process:
26
+ def __init__(
27
+ self,
28
+ config: Config,
29
+ target: Callable[[list[socket] | None], None],
30
+ sockets: list[socket],
31
+ ) -> None:
32
+ self.real_target = target
33
+
34
+ self.parent_conn, self.child_conn = Pipe()
35
+ self.process = get_subprocess(config, self.target, sockets)
36
+
37
+ def ping(self, timeout: float = 5) -> bool:
38
+ self.parent_conn.send(b"ping")
39
+ if self.parent_conn.poll(timeout):
40
+ self.parent_conn.recv()
41
+ return True
42
+ return False
43
+
44
+ def pong(self) -> None:
45
+ self.child_conn.recv()
46
+ self.child_conn.send(b"pong")
47
+
48
+ def always_pong(self) -> None:
49
+ while True:
50
+ self.pong()
51
+
52
+ def target(self, sockets: list[socket] | None = None) -> Any: # pragma: no cover
53
+ if os.name == "nt": # pragma: py-not-win32
54
+ # Windows doesn't support SIGTERM, so we use SIGBREAK instead.
55
+ # And then we raise SIGTERM when SIGBREAK is received.
56
+ # https://learn.microsoft.com/zh-cn/cpp/c-runtime-library/reference/signal?view=msvc-170
57
+ signal.signal(
58
+ signal.SIGBREAK, # type: ignore[attr-defined]
59
+ lambda sig, frame: signal.raise_signal(signal.SIGTERM),
60
+ )
61
+
62
+ threading.Thread(target=self.always_pong, daemon=True).start()
63
+ return self.real_target(sockets)
64
+
65
+ def is_alive(self, timeout: float = 5) -> bool:
66
+ if not self.process.is_alive():
67
+ return False
68
+
69
+ return self.ping(timeout)
70
+
71
+ def start(self) -> None:
72
+ self.process.start()
73
+
74
+ def terminate(self) -> None:
75
+ if self.process.exitcode is None: # Process is still running
76
+ assert self.process.pid is not None
77
+ if os.name == "nt": # pragma: py-not-win32
78
+ # Windows doesn't support SIGTERM.
79
+ # So send SIGBREAK, and then in process raise SIGTERM.
80
+ os.kill(self.process.pid, signal.CTRL_BREAK_EVENT) # type: ignore[attr-defined]
81
+ else:
82
+ os.kill(self.process.pid, signal.SIGTERM)
83
+ logger.info(f"Terminated child process [{self.process.pid}]")
84
+
85
+ self.parent_conn.close()
86
+ self.child_conn.close()
87
+
88
+ def kill(self) -> None:
89
+ # In Windows, the method will call `TerminateProcess` to kill the process.
90
+ # In Unix, the method will send SIGKILL to the process.
91
+ self.process.kill()
92
+
93
+ def join(self) -> None:
94
+ logger.info(f"Waiting for child process [{self.process.pid}]")
95
+ self.process.join()
96
+
97
+ @property
98
+ def pid(self) -> int | None:
99
+ return self.process.pid
100
+
101
+
102
+ class Multiprocess:
103
+ def __init__(
104
+ self,
105
+ config: Config,
106
+ target: Callable[[list[socket] | None], None],
107
+ sockets: list[socket],
108
+ ) -> None:
109
+ self.config = config
110
+ self.target = target
111
+ self.sockets = sockets
112
+
113
+ self.processes_num = config.workers
114
+ self.processes: list[Process] = []
115
+
116
+ self.should_exit = threading.Event()
117
+
118
+ self.signal_queue: list[int] = []
119
+ for sig in SIGNALS:
120
+ signal.signal(sig, lambda sig, frame: self.signal_queue.append(sig))
121
+
122
+ def init_processes(self) -> None:
123
+ for _ in range(self.processes_num):
124
+ process = Process(self.config, self.target, self.sockets)
125
+ process.start()
126
+ self.processes.append(process)
127
+
128
+ def terminate_all(self) -> None:
129
+ for process in self.processes:
130
+ process.terminate()
131
+
132
+ def join_all(self) -> None:
133
+ for process in self.processes:
134
+ process.join()
135
+
136
+ def restart_all(self) -> None:
137
+ for idx, process in enumerate(tuple(self.processes)):
138
+ process.terminate()
139
+ process.join()
140
+ new_process = Process(self.config, self.target, self.sockets)
141
+ new_process.start()
142
+ self.processes[idx] = new_process
143
+
144
+ def run(self) -> None:
145
+ message = f"Started parent process [{os.getpid()}]"
146
+ color_message = "Started parent process [{}]".format(click.style(str(os.getpid()), fg="cyan", bold=True))
147
+ logger.info(message, extra={"color_message": color_message})
148
+
149
+ self.init_processes()
150
+
151
+ while not self.should_exit.wait(0.5):
152
+ self.handle_signals()
153
+ self.keep_subprocess_alive()
154
+
155
+ self.terminate_all()
156
+ self.join_all()
157
+
158
+ message = f"Stopping parent process [{os.getpid()}]"
159
+ color_message = "Stopping parent process [{}]".format(click.style(str(os.getpid()), fg="cyan", bold=True))
160
+ logger.info(message, extra={"color_message": color_message})
161
+
162
+ def keep_subprocess_alive(self) -> None:
163
+ if self.should_exit.is_set():
164
+ return # parent process is exiting, no need to keep subprocess alive
165
+
166
+ for idx, process in enumerate(tuple(self.processes)):
167
+ if process.is_alive():
168
+ continue
169
+
170
+ process.kill() # process is hung, kill it
171
+ process.join()
172
+
173
+ if self.should_exit.is_set():
174
+ return
175
+
176
+ logger.info(f"Child process [{process.pid}] died")
177
+ del self.processes[idx]
178
+ process = Process(self.config, self.target, self.sockets)
179
+ process.start()
180
+ self.processes.append(process)
181
+
182
+ def handle_signals(self) -> None:
183
+ for sig in tuple(self.signal_queue):
184
+ self.signal_queue.remove(sig)
185
+ sig_name = SIGNALS[sig]
186
+ sig_handler = getattr(self, f"handle_{sig_name.lower()}", None)
187
+ if sig_handler is not None:
188
+ sig_handler()
189
+ else: # pragma: no cover
190
+ logger.debug(f"Received signal {sig_name}, but no handler is defined for it.")
191
+
192
+ def handle_int(self) -> None:
193
+ logger.info("Received SIGINT, exiting.")
194
+ self.should_exit.set()
195
+
196
+ def handle_term(self) -> None:
197
+ logger.info("Received SIGTERM, exiting.")
198
+ self.should_exit.set()
199
+
200
+ def handle_break(self) -> None: # pragma: py-not-win32
201
+ logger.info("Received SIGBREAK, exiting.")
202
+ self.should_exit.set()
203
+
204
+ def handle_hup(self) -> None: # pragma: py-win32
205
+ logger.info("Received SIGHUP, restarting processes.")
206
+ self.restart_all()
207
+
208
+ def handle_ttin(self) -> None: # pragma: py-win32
209
+ logger.info("Received SIGTTIN, increasing the number of processes.")
210
+ self.processes_num += 1
211
+ process = Process(self.config, self.target, self.sockets)
212
+ process.start()
213
+ self.processes.append(process)
214
+
215
+ def handle_ttou(self) -> None: # pragma: py-win32
216
+ logger.info("Received SIGTTOU, decreasing number of processes.")
217
+ if self.processes_num <= 1:
218
+ logger.info("Already reached one process, cannot decrease the number of processes anymore.")
219
+ return
220
+ self.processes_num -= 1
221
+ process = self.processes.pop()
222
+ process.terminate()
223
+ process.join()
@@ -4,6 +4,7 @@ import asyncio
4
4
  import logging
5
5
  import signal
6
6
  import sys
7
+ import warnings
7
8
  from typing import Any
8
9
 
9
10
  from gunicorn.arbiter import Arbiter
@@ -12,6 +13,12 @@ from gunicorn.workers.base import Worker
12
13
  from uvicorn.config import Config
13
14
  from uvicorn.main import Server
14
15
 
16
+ warnings.warn(
17
+ "The `uvicorn.workers` module is deprecated. Please use `uvicorn-worker` package instead.\n"
18
+ "For more details, see https://github.com/Kludex/uvicorn-worker.",
19
+ DeprecationWarning,
20
+ )
21
+
15
22
 
16
23
  class UvicornWorker(Worker):
17
24
  """
@@ -1,70 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import logging
4
- import os
5
- import signal
6
- import threading
7
- from multiprocessing.context import SpawnProcess
8
- from socket import socket
9
- from types import FrameType
10
- from typing import Callable
11
-
12
- import click
13
-
14
- from uvicorn._subprocess import get_subprocess
15
- from uvicorn.config import Config
16
-
17
- HANDLED_SIGNALS = (
18
- signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
19
- signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`.
20
- )
21
-
22
- logger = logging.getLogger("uvicorn.error")
23
-
24
-
25
- class Multiprocess:
26
- def __init__(
27
- self,
28
- config: Config,
29
- target: Callable[[list[socket] | None], None],
30
- sockets: list[socket],
31
- ) -> None:
32
- self.config = config
33
- self.target = target
34
- self.sockets = sockets
35
- self.processes: list[SpawnProcess] = []
36
- self.should_exit = threading.Event()
37
- self.pid = os.getpid()
38
-
39
- def signal_handler(self, sig: int, frame: FrameType | None) -> None:
40
- """
41
- A signal handler that is registered with the parent process.
42
- """
43
- self.should_exit.set()
44
-
45
- def run(self) -> None:
46
- self.startup()
47
- self.should_exit.wait()
48
- self.shutdown()
49
-
50
- def startup(self) -> None:
51
- message = f"Started parent process [{str(self.pid)}]"
52
- color_message = "Started parent process [{}]".format(click.style(str(self.pid), fg="cyan", bold=True))
53
- logger.info(message, extra={"color_message": color_message})
54
-
55
- for sig in HANDLED_SIGNALS:
56
- signal.signal(sig, self.signal_handler)
57
-
58
- for _idx in range(self.config.workers):
59
- process = get_subprocess(config=self.config, target=self.target, sockets=self.sockets)
60
- process.start()
61
- self.processes.append(process)
62
-
63
- def shutdown(self) -> None:
64
- for process in self.processes:
65
- process.terminate()
66
- process.join()
67
-
68
- message = f"Stopping parent process [{str(self.pid)}]"
69
- color_message = "Stopping parent process [{}]".format(click.style(str(self.pid), fg="cyan", bold=True))
70
- logger.info(message, extra={"color_message": color_message})
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes