uvicorn 0.29.0__py3-none-any.whl → 0.30.0__py3-none-any.whl

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.
uvicorn/__init__.py CHANGED
@@ -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.29.0"
4
+ __version__ = "0.30.0"
5
5
  __all__ = ["main", "run", "Config", "Server"]
uvicorn/_subprocess.py CHANGED
@@ -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
uvicorn/_types.py CHANGED
@@ -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]
uvicorn/config.py CHANGED
@@ -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
uvicorn/main.py CHANGED
@@ -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:
@@ -4,24 +4,101 @@ import logging
4
4
  import os
5
5
  import signal
6
6
  import threading
7
- from multiprocessing.context import SpawnProcess
7
+ from multiprocessing import Pipe
8
8
  from socket import socket
9
- from types import FrameType
10
- from typing import Callable
9
+ from typing import Any, Callable
11
10
 
12
11
  import click
13
12
 
14
13
  from uvicorn._subprocess import get_subprocess
15
14
  from uvicorn.config import Config
16
15
 
17
- HANDLED_SIGNALS = (
18
- signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
19
- signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`.
20
- )
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
21
 
22
22
  logger = logging.getLogger("uvicorn.error")
23
23
 
24
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
+
25
102
  class Multiprocess:
26
103
  def __init__(
27
104
  self,
@@ -32,39 +109,115 @@ class Multiprocess:
32
109
  self.config = config
33
110
  self.target = target
34
111
  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
112
 
45
- def run(self) -> None:
46
- self.startup()
47
- self.should_exit.wait()
48
- self.shutdown()
113
+ self.processes_num = config.workers
114
+ self.processes: list[Process] = []
49
115
 
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})
116
+ self.should_exit = threading.Event()
54
117
 
55
- for sig in HANDLED_SIGNALS:
56
- signal.signal(sig, self.signal_handler)
118
+ self.signal_queue: list[int] = []
119
+ for sig in SIGNALS:
120
+ signal.signal(sig, lambda sig, frame: self.signal_queue.append(sig))
57
121
 
58
- for _idx in range(self.config.workers):
59
- process = get_subprocess(config=self.config, target=self.target, sockets=self.sockets)
122
+ def init_processes(self) -> None:
123
+ for _ in range(self.processes_num):
124
+ process = Process(self.config, self.target, self.sockets)
60
125
  process.start()
61
126
  self.processes.append(process)
62
127
 
63
- def shutdown(self) -> None:
128
+ def terminate_all(self) -> None:
64
129
  for process in self.processes:
65
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()
66
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})
67
148
 
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))
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))
70
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()
uvicorn/workers.py CHANGED
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: uvicorn
3
- Version: 0.29.0
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
@@ -1,14 +1,14 @@
1
- uvicorn/__init__.py,sha256=k39dO_sJjc5r3ZBUCrR15zL0qmWLLSJMhsiN_NDAiKo,147
1
+ uvicorn/__init__.py,sha256=vTrtzT1tyLLpXdJOdibN6a_QQYT8gZ4yAepJkXz_bl0,147
2
2
  uvicorn/__main__.py,sha256=DQizy6nKP0ywhPpnCHgmRDYIMfcqZKVEzNIWQZjqtVQ,62
3
- uvicorn/_subprocess.py,sha256=wIxSuARuoouCXdLAVQCc2-MNv9utuTCpD9BsmLeqsvE,2505
4
- uvicorn/_types.py,sha256=uJ4IRbis4frxIuO2mfCQu2OCToLRSFKp8foKIGwMjII,7793
5
- uvicorn/config.py,sha256=I0YgdIbnwcQB5OGcSvkeDHRI-ChdmjGy5ab1cNKk6ak,20524
3
+ uvicorn/_subprocess.py,sha256=wc7tS3hmHLX9RHBJchu0ZHjUeQEuXehi3xvQvK4uUTY,2741
4
+ uvicorn/_types.py,sha256=KzJumVocO3k6pAFvLTSHtug9TqO9CC2cbe5r1NvJlbA,7778
5
+ uvicorn/config.py,sha256=gk9sg4XHc1FcUbx9mN56kAfKN4ERMKM_qc3V9qlVPOI,20671
6
6
  uvicorn/importer.py,sha256=nRt0QQ3qpi264-n_mR0l55C2ddM8nowTNzT1jsWaam8,1128
7
7
  uvicorn/logging.py,sha256=sg4D9lHaW_kKQj_kmP-bolbChjKfhBuihktlWp8RjSI,4236
8
- uvicorn/main.py,sha256=uixfAm9Mfymxr2ZCA_YNy9avkD9Ljyv5onaSgF_bMZc,16565
8
+ uvicorn/main.py,sha256=Ugv-CHZ1IBEuXXoXOE4J2MjDzPZX3XA4FSqUqvyYclU,16715
9
9
  uvicorn/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
10
10
  uvicorn/server.py,sha256=bXpsRamwT2cRoe7u6Nk8Czp_la2sfSnV9V0dva5-gkE,12729
11
- uvicorn/workers.py,sha256=WhPoxhxvwebxP69DCfNYo_1mIOC7nIUTeC0wrrjiL3g,3667
11
+ uvicorn/workers.py,sha256=DukTKlrCyyvWVHbJWBJflIV2yUe-q6KaGdrEwLrNmyc,3893
12
12
  uvicorn/lifespan/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  uvicorn/lifespan/off.py,sha256=nfI6qHAUo_8-BEXMBKoHQ9wUbsXrPaXLCbDSS0vKSr8,332
14
14
  uvicorn/lifespan/on.py,sha256=1KYuFNNyQONIjtEHhKZAJp-OOokIyjj74wpGCGBv4lk,5184
@@ -19,27 +19,27 @@ uvicorn/loops/uvloop.py,sha256=K4QybYVxtK9C2emDhDPUCkBXR4XMT5Ofv9BPFPoX0ok,148
19
19
  uvicorn/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  uvicorn/middleware/asgi2.py,sha256=YQrQNm3RehFts3mzk3k4yw8aD8Egtj0tRS3N45YkQa0,394
21
21
  uvicorn/middleware/message_logger.py,sha256=IHEZUSnFNaMFUFdwtZO3AuFATnYcSor-gVtOjbCzt8M,2859
22
- uvicorn/middleware/proxy_headers.py,sha256=-ZHb6hcAe9L--7lQ2waBhZII_W80I4cMd2JqOyYuzdk,3039
22
+ uvicorn/middleware/proxy_headers.py,sha256=CT7cYPf1FmmAWxnpKwLYPLPNBM6WxgU2NqYYZrmXPWQ,3040
23
23
  uvicorn/middleware/wsgi.py,sha256=TBeG4W_gEmWddbGfWyxdzJ0IDaWWkJZyF8eIp-1fv0U,7111
24
24
  uvicorn/protocols/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
- uvicorn/protocols/utils.py,sha256=Gt3UTpWnMNduq-hif4UocEkWjdkw7Us0OxMrejHIElA,1853
25
+ uvicorn/protocols/utils.py,sha256=kBwTa7T6Ed1-tSpNfLWhJjxhTbH163GqPwwAcXshPVc,1849
26
26
  uvicorn/protocols/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
27
  uvicorn/protocols/http/auto.py,sha256=YfXGyzWTaaE2p_jkTPWrJCXsxEaQnC3NK0-G7Wgmnls,403
28
- uvicorn/protocols/http/flow_control.py,sha256=yFt2GpEfugpuPiwCL7bAh8hd9Eub8V8THUJQ8ttDiiU,1771
29
- uvicorn/protocols/http/h11_impl.py,sha256=fJ9KeGIme-OdLxt7_wrm5VwlDIFtWMw00edgjgN2P0E,20376
30
- uvicorn/protocols/http/httptools_impl.py,sha256=Ei77FJPorqq8d3C-YGUe9eTLDnX6afaBsoEfj7xtE5Y,21462
28
+ uvicorn/protocols/http/flow_control.py,sha256=lwEBY3zsxJrxykud6OB-jQQd9rUQkFXDmqQyPGBm5ag,1626
29
+ uvicorn/protocols/http/h11_impl.py,sha256=TycBaEdfs74j2t-EqsOHpd1s0RNDx6wYpgIaQVd9zm8,20330
30
+ uvicorn/protocols/http/httptools_impl.py,sha256=owVsTiJ7lSip4LoIpXAuEBdVabnjmsQdhdHDuPD0uB8,21291
31
31
  uvicorn/protocols/websockets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  uvicorn/protocols/websockets/auto.py,sha256=kNP-h07ZzjA9dKRUd7MNO0J7xhRJ5xVBfit7wCbdB0A,574
33
- uvicorn/protocols/websockets/websockets_impl.py,sha256=fIbUdUJZZhx6KdBBj8nG56pkZtNeMWj_QxRgg17bmtQ,15234
34
- uvicorn/protocols/websockets/wsproto_impl.py,sha256=i7bO9CctoUEwMilg760NgdToHgW0466vk9deR-LLo6E,15130
33
+ uvicorn/protocols/websockets/websockets_impl.py,sha256=bagg9R71eBqb5EyB2tUV3OLuw8uBhPZUuVykgzfrki4,15457
34
+ uvicorn/protocols/websockets/wsproto_impl.py,sha256=sktHUkyrpdlwsai_tQ8mahy248KZQ9iJaeLXkZ5rfWI,15199
35
35
  uvicorn/supervisors/__init__.py,sha256=UVJYW3RVHMDSgUytToyAgGyd9NUQVqbNpVrQrvm4Tpc,700
36
36
  uvicorn/supervisors/basereload.py,sha256=u83LepHT28QFH84wwsxJypwBKO1apG4c4WziPMfOqmE,3850
37
- uvicorn/supervisors/multiprocess.py,sha256=vBGBBq4vQcSdghev-kGjtBPGMwCyTNs1IE6NeGxQ76s,2156
37
+ uvicorn/supervisors/multiprocess.py,sha256=BeYZ8vgRfFLSKDHC59QXutiNhM7Kw-ACMvw9dHXnrwE,7508
38
38
  uvicorn/supervisors/statreload.py,sha256=gc-HUB44f811PvxD_ZIEQYenM7mWmhQQjYg7KKQ1c5o,1542
39
39
  uvicorn/supervisors/watchfilesreload.py,sha256=RMhWgInlOr0MJB0RvmW50RZY1ls9Kp9VT3eaLjdRTpw,2935
40
40
  uvicorn/supervisors/watchgodreload.py,sha256=kd-gOvp14ArTNIc206Nt5CEjZZ4NP2UmMVYE7571yRQ,5486
41
- uvicorn-0.29.0.dist-info/METADATA,sha256=7eFKWJYud857eJfc05LZwrXIOAWcwdyFZN8pMqo-afM,6330
42
- uvicorn-0.29.0.dist-info/WHEEL,sha256=bq9SyP5NxIRA9EpQgMCd-9RmPHWvbH-4lTDGwxgIR64,87
43
- uvicorn-0.29.0.dist-info/entry_points.txt,sha256=FW1w-hkc9QgwaGoovMvm0ZY73w_NcycWdGAUfDsNGxw,46
44
- uvicorn-0.29.0.dist-info/licenses/LICENSE.md,sha256=7-Gs8-YvuZwoiw7HPlp3O3Jo70Mg_nV-qZQhTktjw3E,1526
45
- uvicorn-0.29.0.dist-info/RECORD,,
41
+ uvicorn-0.30.0.dist-info/METADATA,sha256=qiB2nEtFeOiRCSeMiAm8BJlXdEMs6Wk07FFQbEoq17M,6330
42
+ uvicorn-0.30.0.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
43
+ uvicorn-0.30.0.dist-info/entry_points.txt,sha256=FW1w-hkc9QgwaGoovMvm0ZY73w_NcycWdGAUfDsNGxw,46
44
+ uvicorn-0.30.0.dist-info/licenses/LICENSE.md,sha256=7-Gs8-YvuZwoiw7HPlp3O3Jo70Mg_nV-qZQhTktjw3E,1526
45
+ uvicorn-0.30.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.22.3
2
+ Generator: hatchling 1.24.2
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any