uvicorn 0.32.1__tar.gz → 0.34.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 (78) hide show
  1. {uvicorn-0.32.1 → uvicorn-0.34.0}/PKG-INFO +2 -3
  2. {uvicorn-0.32.1 → uvicorn-0.34.0}/pyproject.toml +5 -13
  3. {uvicorn-0.32.1 → uvicorn-0.34.0}/requirements.txt +10 -11
  4. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/conftest.py +0 -23
  5. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/middleware/test_wsgi.py +3 -2
  6. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/supervisors/test_reload.py +59 -90
  7. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/test_cli.py +1 -1
  8. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/test_server.py +4 -2
  9. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/__init__.py +1 -1
  10. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/_types.py +5 -17
  11. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/config.py +3 -2
  12. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/logging.py +1 -1
  13. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/middleware/wsgi.py +1 -1
  14. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/protocols/websockets/websockets_impl.py +2 -1
  15. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/protocols/websockets/wsproto_impl.py +1 -0
  16. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/server.py +3 -5
  17. uvicorn-0.34.0/uvicorn/supervisors/__init__.py +16 -0
  18. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/supervisors/basereload.py +2 -1
  19. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/supervisors/statreload.py +2 -1
  20. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/workers.py +1 -1
  21. uvicorn-0.32.1/uvicorn/supervisors/__init__.py +0 -23
  22. uvicorn-0.32.1/uvicorn/supervisors/watchgodreload.py +0 -152
  23. {uvicorn-0.32.1 → uvicorn-0.34.0}/.gitignore +0 -0
  24. {uvicorn-0.32.1 → uvicorn-0.34.0}/LICENSE.md +0 -0
  25. {uvicorn-0.32.1 → uvicorn-0.34.0}/README.md +0 -0
  26. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/__init__.py +0 -0
  27. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/importer/__init__.py +0 -0
  28. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/importer/circular_import_a.py +0 -0
  29. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/importer/circular_import_b.py +0 -0
  30. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/importer/raise_import_error.py +0 -0
  31. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/importer/test_importer.py +0 -0
  32. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/middleware/__init__.py +0 -0
  33. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/middleware/test_logging.py +0 -0
  34. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/middleware/test_message_logger.py +0 -0
  35. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/middleware/test_proxy_headers.py +0 -0
  36. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/protocols/__init__.py +0 -0
  37. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/protocols/test_http.py +0 -0
  38. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/protocols/test_utils.py +0 -0
  39. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/protocols/test_websocket.py +0 -0
  40. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/response.py +0 -0
  41. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/supervisors/__init__.py +0 -0
  42. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/supervisors/test_multiprocess.py +0 -0
  43. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/supervisors/test_signal.py +0 -0
  44. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/test_auto_detection.py +0 -0
  45. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/test_config.py +0 -0
  46. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/test_default_headers.py +0 -0
  47. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/test_lifespan.py +0 -0
  48. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/test_main.py +0 -0
  49. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/test_ssl.py +0 -0
  50. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/test_subprocess.py +0 -0
  51. {uvicorn-0.32.1 → uvicorn-0.34.0}/tests/utils.py +0 -0
  52. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/__main__.py +0 -0
  53. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/_subprocess.py +0 -0
  54. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/importer.py +0 -0
  55. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/lifespan/__init__.py +0 -0
  56. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/lifespan/off.py +0 -0
  57. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/lifespan/on.py +0 -0
  58. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/loops/__init__.py +0 -0
  59. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/loops/asyncio.py +0 -0
  60. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/loops/auto.py +0 -0
  61. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/loops/uvloop.py +0 -0
  62. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/main.py +0 -0
  63. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/middleware/__init__.py +0 -0
  64. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/middleware/asgi2.py +0 -0
  65. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/middleware/message_logger.py +0 -0
  66. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/middleware/proxy_headers.py +0 -0
  67. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/protocols/__init__.py +0 -0
  68. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/protocols/http/__init__.py +0 -0
  69. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/protocols/http/auto.py +0 -0
  70. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/protocols/http/flow_control.py +0 -0
  71. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/protocols/http/h11_impl.py +0 -0
  72. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/protocols/http/httptools_impl.py +0 -0
  73. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/protocols/utils.py +0 -0
  74. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/protocols/websockets/__init__.py +0 -0
  75. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/protocols/websockets/auto.py +0 -0
  76. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/py.typed +0 -0
  77. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/supervisors/multiprocess.py +0 -0
  78. {uvicorn-0.32.1 → uvicorn-0.34.0}/uvicorn/supervisors/watchfilesreload.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: uvicorn
3
- Version: 0.32.1
3
+ Version: 0.34.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
@@ -14,7 +14,6 @@ Classifier: Intended Audience :: Developers
14
14
  Classifier: License :: OSI Approved :: BSD License
15
15
  Classifier: Operating System :: OS Independent
16
16
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.8
18
17
  Classifier: Programming Language :: Python :: 3.9
19
18
  Classifier: Programming Language :: Python :: 3.10
20
19
  Classifier: Programming Language :: Python :: 3.11
@@ -23,7 +22,7 @@ Classifier: Programming Language :: Python :: 3.13
23
22
  Classifier: Programming Language :: Python :: Implementation :: CPython
24
23
  Classifier: Programming Language :: Python :: Implementation :: PyPy
25
24
  Classifier: Topic :: Internet :: WWW/HTTP
26
- Requires-Python: >=3.8
25
+ Requires-Python: >=3.9
27
26
  Requires-Dist: click>=7.0
28
27
  Requires-Dist: h11>=0.8
29
28
  Requires-Dist: typing-extensions>=4.0; python_version < '3.11'
@@ -8,10 +8,10 @@ dynamic = ["version"]
8
8
  description = "The lightning-fast ASGI server."
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
11
- requires-python = ">=3.8"
11
+ requires-python = ">=3.9"
12
12
  authors = [
13
13
  { name = "Tom Christie", email = "tom@tomchristie.com" },
14
- { name = "Marcelo Trylesinski", email = "marcelotryle@gmail.com" }
14
+ { name = "Marcelo Trylesinski", email = "marcelotryle@gmail.com" },
15
15
  ]
16
16
  classifiers = [
17
17
  "Development Status :: 4 - Beta",
@@ -20,7 +20,6 @@ classifiers = [
20
20
  "License :: OSI Approved :: BSD License",
21
21
  "Operating System :: OS Independent",
22
22
  "Programming Language :: Python :: 3",
23
- "Programming Language :: Python :: 3.8",
24
23
  "Programming Language :: Python :: 3.9",
25
24
  "Programming Language :: Python :: 3.10",
26
25
  "Programming Language :: Python :: 3.11",
@@ -38,7 +37,7 @@ dependencies = [
38
37
 
39
38
  [project.optional-dependencies]
40
39
  standard = [
41
- "colorama>=0.4;sys_platform == 'win32'",
40
+ "colorama>=0.4; sys_platform == 'win32'",
42
41
  "httptools>=0.6.3",
43
42
  "python-dotenv>=0.13",
44
43
  "PyYAML>=5.1",
@@ -60,11 +59,7 @@ Source = "https://github.com/encode/uvicorn"
60
59
  path = "uvicorn/__init__.py"
61
60
 
62
61
  [tool.hatch.build.targets.sdist]
63
- include = [
64
- "/uvicorn",
65
- "/tests",
66
- "/requirements.txt",
67
- ]
62
+ include = ["/uvicorn", "/tests", "/requirements.txt"]
68
63
 
69
64
  [tool.ruff]
70
65
  line-length = 120
@@ -94,10 +89,9 @@ addopts = "-rxXs --strict-config --strict-markers"
94
89
  xfail_strict = true
95
90
  filterwarnings = [
96
91
  "error",
97
- 'ignore: \"watchgod\" is deprecated\, you should switch to watchfiles \(`pip install watchfiles`\)\.:DeprecationWarning',
98
92
  "ignore:Uvicorn's native WSGI implementation is deprecated.*:DeprecationWarning",
99
93
  "ignore: 'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning",
100
- "ignore: remove second argument of ws_handler:DeprecationWarning:websockets"
94
+ "ignore: remove second argument of ws_handler:DeprecationWarning:websockets",
101
95
  ]
102
96
 
103
97
  [tool.coverage.run]
@@ -132,8 +126,6 @@ py-win32 = "sys_platform == 'win32'"
132
126
  py-not-win32 = "sys_platform != 'win32'"
133
127
  py-linux = "sys_platform == 'linux'"
134
128
  py-darwin = "sys_platform == 'darwin'"
135
- py-gte-38 = "sys_version_info >= (3, 8)"
136
- py-lt-38 = "sys_version_info < (3, 8)"
137
129
  py-gte-39 = "sys_version_info >= (3, 9)"
138
130
  py-lt-39 = "sys_version_info < (3, 9)"
139
131
  py-gte-310 = "sys_version_info >= (3, 10)"
@@ -10,23 +10,22 @@ wsproto==1.2.0
10
10
  websockets==13.1
11
11
 
12
12
  # Packaging
13
- build==1.2.2
14
- twine==5.1.1
13
+ build==1.2.2.post1
14
+ twine==6.0.1
15
15
 
16
16
  # Testing
17
- ruff==0.6.8
18
- pytest==8.3.3
17
+ ruff==0.8.3
18
+ pytest==8.3.4
19
19
  pytest-mock==3.14.0
20
- mypy==1.11.2
20
+ mypy==1.13.0
21
21
  types-click==7.1.8
22
22
  types-pyyaml==6.0.12.20240917
23
- trustme==1.1.0
24
- cryptography==43.0.1
25
- coverage==7.6.1
23
+ trustme==1.2.0
24
+ cryptography==44.0.0
25
+ coverage==7.6.9
26
26
  coverage-conditional-plugin==0.9.0
27
- httpx==0.27.2
28
- watchgod==0.8.2
27
+ httpx==0.28.1
29
28
 
30
29
  # Documentation
31
30
  mkdocs==1.6.1
32
- mkdocs-material==9.5.39
31
+ mkdocs-material==9.5.48
@@ -9,8 +9,6 @@ from copy import deepcopy
9
9
  from hashlib import md5
10
10
  from pathlib import Path
11
11
  from tempfile import TemporaryDirectory
12
- from threading import Thread
13
- from time import sleep
14
12
  from typing import Any
15
13
  from uuid import uuid4
16
14
 
@@ -214,27 +212,6 @@ def short_socket_name(tmp_path, tmp_path_factory): # pragma: py-win32
214
212
  return
215
213
 
216
214
 
217
- def sleep_touch(*paths: Path):
218
- sleep(0.1)
219
- for p in paths:
220
- p.touch()
221
-
222
-
223
- @pytest.fixture
224
- def touch_soon():
225
- threads = []
226
-
227
- def start(*paths: Path):
228
- thread = Thread(target=sleep_touch, args=paths)
229
- thread.start()
230
- threads.append(thread)
231
-
232
- yield start
233
-
234
- for t in threads:
235
- t.join()
236
-
237
-
238
215
  def _unused_port(socket_type: int) -> int:
239
216
  """Find an unused localhost port from 1024-65535 and return it."""
240
217
  with contextlib.closing(socket.socket(type=socket_type)) as sock:
@@ -2,7 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  import io
4
4
  import sys
5
- from typing import AsyncGenerator, Callable
5
+ from collections.abc import AsyncGenerator
6
+ from typing import Callable
6
7
 
7
8
  import a2wsgi
8
9
  import httpx
@@ -72,7 +73,7 @@ async def test_wsgi_post(wsgi_middleware: Callable) -> None:
72
73
  async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
73
74
  response = await client.post("/", json={"example": 123})
74
75
  assert response.status_code == 200
75
- assert response.text == '{"example": 123}'
76
+ assert response.text == '{"example":123}'
76
77
 
77
78
 
78
79
  @pytest.mark.anyio
@@ -1,14 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
- import logging
4
3
  import platform
5
4
  import signal
6
5
  import socket
7
6
  import sys
7
+ from collections.abc import Generator
8
8
  from pathlib import Path
9
+ from threading import Thread
9
10
  from time import sleep
11
+ from typing import Callable
10
12
 
11
13
  import pytest
14
+ from pytest_mock import MockerFixture
12
15
 
13
16
  from tests.utils import as_cwd
14
17
  from uvicorn.config import Config
@@ -20,11 +23,6 @@ try:
20
23
  except ImportError: # pragma: no cover
21
24
  WatchFilesReload = None # type: ignore[misc,assignment]
22
25
 
23
- try:
24
- from uvicorn.supervisors.watchgodreload import WatchGodReload
25
- except ImportError: # pragma: no cover
26
- WatchGodReload = None # type: ignore[misc,assignment]
27
-
28
26
 
29
27
  # TODO: Investigate why this is flaky on MacOS M1.
30
28
  skip_if_m1 = pytest.mark.skipif(
@@ -33,17 +31,34 @@ skip_if_m1 = pytest.mark.skipif(
33
31
  )
34
32
 
35
33
 
36
- def run(sockets):
34
+ def run(sockets: list[socket.socket] | None) -> None:
37
35
  pass # pragma: no cover
38
36
 
39
37
 
38
+ def sleep_touch(*paths: Path):
39
+ sleep(0.1)
40
+ for p in paths:
41
+ p.touch()
42
+
43
+
44
+ @pytest.fixture
45
+ def touch_soon() -> Generator[Callable[[Path], None]]:
46
+ threads: list[Thread] = []
47
+
48
+ def start(*paths: Path) -> None:
49
+ thread = Thread(target=sleep_touch, args=paths)
50
+ thread.start()
51
+ threads.append(thread)
52
+
53
+ yield start
54
+
55
+ for t in threads:
56
+ t.join()
57
+
58
+
40
59
  class TestBaseReload:
41
60
  @pytest.fixture(autouse=True)
42
- def setup(
43
- self,
44
- reload_directory_structure: Path,
45
- reloader_class: type[BaseReload] | None,
46
- ):
61
+ def setup(self, reload_directory_structure: Path, reloader_class: type[BaseReload] | None):
47
62
  if reloader_class is None: # pragma: no cover
48
63
  pytest.skip("Needed dependency not installed")
49
64
  self.reload_path = reload_directory_structure
@@ -52,17 +67,15 @@ class TestBaseReload:
52
67
  def _setup_reloader(self, config: Config) -> BaseReload:
53
68
  config.reload_delay = 0 # save time
54
69
 
55
- if self.reloader_class is WatchGodReload:
56
- with pytest.deprecated_call():
57
- reloader = self.reloader_class(config, target=run, sockets=[])
58
- else:
59
- reloader = self.reloader_class(config, target=run, sockets=[])
70
+ reloader = self.reloader_class(config, target=run, sockets=[])
60
71
 
61
72
  assert config.should_reload
62
73
  reloader.startup()
63
74
  return reloader
64
75
 
65
- def _reload_tester(self, touch_soon, reloader: BaseReload, *files: Path) -> list[Path] | None:
76
+ def _reload_tester(
77
+ self, touch_soon: Callable[[Path], None], reloader: BaseReload, *files: Path
78
+ ) -> list[Path] | None:
66
79
  reloader.restart()
67
80
  if WatchFilesReload is not None and isinstance(reloader, WatchFilesReload):
68
81
  touch_soon(*files)
@@ -73,7 +86,7 @@ class TestBaseReload:
73
86
  file.touch()
74
87
  return next(reloader)
75
88
 
76
- @pytest.mark.parametrize("reloader_class", [StatReload, WatchGodReload, WatchFilesReload])
89
+ @pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
77
90
  def test_reloader_should_initialize(self) -> None:
78
91
  """
79
92
  A basic sanity check.
@@ -86,8 +99,8 @@ class TestBaseReload:
86
99
  reloader = self._setup_reloader(config)
87
100
  reloader.shutdown()
88
101
 
89
- @pytest.mark.parametrize("reloader_class", [StatReload, WatchGodReload, WatchFilesReload])
90
- def test_reload_when_python_file_is_changed(self, touch_soon) -> None:
102
+ @pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
103
+ def test_reload_when_python_file_is_changed(self, touch_soon: Callable[[Path], None]):
91
104
  file = self.reload_path / "main.py"
92
105
 
93
106
  with as_cwd(self.reload_path):
@@ -99,8 +112,8 @@ class TestBaseReload:
99
112
 
100
113
  reloader.shutdown()
101
114
 
102
- @pytest.mark.parametrize("reloader_class", [StatReload, WatchGodReload, WatchFilesReload])
103
- def test_should_reload_when_python_file_in_subdir_is_changed(self, touch_soon) -> None:
115
+ @pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
116
+ def test_should_reload_when_python_file_in_subdir_is_changed(self, touch_soon: Callable[[Path], None]):
104
117
  file = self.reload_path / "app" / "sub" / "sub.py"
105
118
 
106
119
  with as_cwd(self.reload_path):
@@ -111,8 +124,8 @@ class TestBaseReload:
111
124
 
112
125
  reloader.shutdown()
113
126
 
114
- @pytest.mark.parametrize("reloader_class", [WatchFilesReload, WatchGodReload])
115
- def test_should_not_reload_when_python_file_in_excluded_subdir_is_changed(self, touch_soon) -> None:
127
+ @pytest.mark.parametrize("reloader_class", [WatchFilesReload])
128
+ def test_should_not_reload_when_python_file_in_excluded_subdir_is_changed(self, touch_soon: Callable[[Path], None]):
116
129
  sub_dir = self.reload_path / "app" / "sub"
117
130
  sub_file = sub_dir / "sub.py"
118
131
 
@@ -129,7 +142,7 @@ class TestBaseReload:
129
142
  reloader.shutdown()
130
143
 
131
144
  @pytest.mark.parametrize("reloader_class, result", [(StatReload, False), (WatchFilesReload, True)])
132
- def test_reload_when_pattern_matched_file_is_changed(self, result: bool, touch_soon) -> None:
145
+ def test_reload_when_pattern_matched_file_is_changed(self, result: bool, touch_soon: Callable[[Path], None]):
133
146
  file = self.reload_path / "app" / "js" / "main.js"
134
147
 
135
148
  with as_cwd(self.reload_path):
@@ -140,14 +153,10 @@ class TestBaseReload:
140
153
 
141
154
  reloader.shutdown()
142
155
 
143
- @pytest.mark.parametrize(
144
- "reloader_class",
145
- [
146
- pytest.param(WatchFilesReload, marks=skip_if_m1),
147
- WatchGodReload,
148
- ],
149
- )
150
- def test_should_not_reload_when_exclude_pattern_match_file_is_changed(self, touch_soon) -> None:
156
+ @pytest.mark.parametrize("reloader_class", [pytest.param(WatchFilesReload, marks=skip_if_m1)])
157
+ def test_should_not_reload_when_exclude_pattern_match_file_is_changed(
158
+ self, touch_soon: Callable[[Path], None]
159
+ ): # pragma: py-darwin
151
160
  python_file = self.reload_path / "app" / "src" / "main.py"
152
161
  css_file = self.reload_path / "app" / "css" / "main.css"
153
162
  js_file = self.reload_path / "app" / "js" / "main.js"
@@ -167,8 +176,8 @@ class TestBaseReload:
167
176
 
168
177
  reloader.shutdown()
169
178
 
170
- @pytest.mark.parametrize("reloader_class", [StatReload, WatchGodReload, WatchFilesReload])
171
- def test_should_not_reload_when_dot_file_is_changed(self, touch_soon) -> None:
179
+ @pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
180
+ def test_should_not_reload_when_dot_file_is_changed(self, touch_soon: Callable[[Path], None]):
172
181
  file = self.reload_path / ".dotted"
173
182
 
174
183
  with as_cwd(self.reload_path):
@@ -179,8 +188,8 @@ class TestBaseReload:
179
188
 
180
189
  reloader.shutdown()
181
190
 
182
- @pytest.mark.parametrize("reloader_class", [StatReload, WatchGodReload, WatchFilesReload])
183
- def test_should_reload_when_directories_have_same_prefix(self, touch_soon) -> None:
191
+ @pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
192
+ def test_should_reload_when_directories_have_same_prefix(self, touch_soon: Callable[[Path], None]):
184
193
  app_dir = self.reload_path / "app"
185
194
  app_file = app_dir / "src" / "main.py"
186
195
  app_first_dir = self.reload_path / "app_first"
@@ -201,13 +210,9 @@ class TestBaseReload:
201
210
 
202
211
  @pytest.mark.parametrize(
203
212
  "reloader_class",
204
- [
205
- StatReload,
206
- WatchGodReload,
207
- pytest.param(WatchFilesReload, marks=skip_if_m1),
208
- ],
213
+ [StatReload, pytest.param(WatchFilesReload, marks=skip_if_m1)],
209
214
  )
210
- def test_should_not_reload_when_only_subdirectory_is_watched(self, touch_soon) -> None:
215
+ def test_should_not_reload_when_only_subdirectory_is_watched(self, touch_soon: Callable[[Path], None]):
211
216
  app_dir = self.reload_path / "app"
212
217
  app_dir_file = self.reload_path / "app" / "src" / "main.py"
213
218
  root_file = self.reload_path / "main.py"
@@ -224,14 +229,8 @@ class TestBaseReload:
224
229
 
225
230
  reloader.shutdown()
226
231
 
227
- @pytest.mark.parametrize(
228
- "reloader_class",
229
- [
230
- pytest.param(WatchFilesReload, marks=skip_if_m1),
231
- WatchGodReload,
232
- ],
233
- )
234
- def test_override_defaults(self, touch_soon) -> None:
232
+ @pytest.mark.parametrize("reloader_class", [pytest.param(WatchFilesReload, marks=skip_if_m1)])
233
+ def test_override_defaults(self, touch_soon: Callable[[Path], None]) -> None: # pragma: py-darwin
235
234
  dotted_file = self.reload_path / ".dotted"
236
235
  dotted_dir_file = self.reload_path / ".dotted_dir" / "file.txt"
237
236
  python_file = self.reload_path / "main.py"
@@ -252,14 +251,8 @@ class TestBaseReload:
252
251
 
253
252
  reloader.shutdown()
254
253
 
255
- @pytest.mark.parametrize(
256
- "reloader_class",
257
- [
258
- pytest.param(WatchFilesReload, marks=skip_if_m1),
259
- WatchGodReload,
260
- ],
261
- )
262
- def test_explicit_paths(self, touch_soon) -> None:
254
+ @pytest.mark.parametrize("reloader_class", [pytest.param(WatchFilesReload, marks=skip_if_m1)])
255
+ def test_explicit_paths(self, touch_soon: Callable[[Path], None]) -> None: # pragma: py-darwin
263
256
  dotted_file = self.reload_path / ".dotted"
264
257
  non_dotted_file = self.reload_path / "ext" / "ext.jpg"
265
258
  python_file = self.reload_path / "main.py"
@@ -307,33 +300,9 @@ class TestBaseReload:
307
300
 
308
301
  reloader.shutdown()
309
302
 
310
- @pytest.mark.parametrize("reloader_class", [WatchGodReload])
311
- def test_should_detect_new_reload_dirs(self, touch_soon, caplog: pytest.LogCaptureFixture, tmp_path: Path) -> None:
312
- app_dir = tmp_path / "app"
313
- app_file = app_dir / "file.py"
314
- app_dir.mkdir()
315
- app_file.touch()
316
- app_first_dir = tmp_path / "app_first"
317
- app_first_file = app_first_dir / "file.py"
318
-
319
- with as_cwd(tmp_path):
320
- config = Config(app="tests.test_config:asgi_app", reload=True, reload_includes=["app*"])
321
- reloader = self._setup_reloader(config)
322
- assert self._reload_tester(touch_soon, reloader, app_file)
323
-
324
- app_first_dir.mkdir()
325
- assert self._reload_tester(touch_soon, reloader, app_first_file)
326
- assert caplog.records[-2].levelno == logging.INFO
327
- assert (
328
- caplog.records[-1].message == "WatchGodReload detected a new reload "
329
- f"dir '{app_first_dir.name}' in '{tmp_path}'; Adding to watch list."
330
- )
331
-
332
- reloader.shutdown()
333
-
334
303
 
335
304
  @pytest.mark.skipif(WatchFilesReload is None, reason="watchfiles not available")
336
- def test_should_watch_one_dir_cwd(mocker, reload_directory_structure):
305
+ def test_should_watch_one_dir_cwd(mocker: MockerFixture, reload_directory_structure: Path):
337
306
  mock_watch = mocker.patch("uvicorn.supervisors.watchfilesreload.watch")
338
307
  app_dir = reload_directory_structure / "app"
339
308
  app_first_dir = reload_directory_structure / "app_first"
@@ -350,7 +319,7 @@ def test_should_watch_one_dir_cwd(mocker, reload_directory_structure):
350
319
 
351
320
 
352
321
  @pytest.mark.skipif(WatchFilesReload is None, reason="watchfiles not available")
353
- def test_should_watch_separate_dirs_outside_cwd(mocker, reload_directory_structure):
322
+ def test_should_watch_separate_dirs_outside_cwd(mocker: MockerFixture, reload_directory_structure: Path):
354
323
  mock_watch = mocker.patch("uvicorn.supervisors.watchfilesreload.watch")
355
324
  app_dir = reload_directory_structure / "app"
356
325
  app_first_dir = reload_directory_structure / "app_first"
@@ -368,7 +337,7 @@ def test_should_watch_separate_dirs_outside_cwd(mocker, reload_directory_structu
368
337
  }
369
338
 
370
339
 
371
- def test_display_path_relative(tmp_path):
340
+ def test_display_path_relative(tmp_path: Path):
372
341
  with as_cwd(tmp_path):
373
342
  p = tmp_path / "app" / "foobar.py"
374
343
  # accept windows paths as wells as posix
@@ -380,8 +349,8 @@ def test_display_path_non_relative():
380
349
  assert _display_path(p) in ("'/foo/bar.py'", "'\\foo\\bar.py'")
381
350
 
382
351
 
383
- def test_base_reloader_run(tmp_path):
384
- calls = []
352
+ def test_base_reloader_run(tmp_path: Path):
353
+ calls: list[str] = []
385
354
  step = 0
386
355
 
387
356
  class CustomReload(BaseReload):
@@ -411,7 +380,7 @@ def test_base_reloader_run(tmp_path):
411
380
  assert calls == ["startup", "restart", "shutdown"]
412
381
 
413
382
 
414
- def test_base_reloader_should_exit(tmp_path):
383
+ def test_base_reloader_should_exit(tmp_path: Path):
415
384
  config = Config(app="tests.test_config:asgi_app", reload=True)
416
385
  reloader = BaseReload(config, target=run, sockets=[])
417
386
  assert not reloader.should_exit.is_set()
@@ -3,9 +3,9 @@ import importlib
3
3
  import os
4
4
  import platform
5
5
  import sys
6
+ from collections.abc import Iterator
6
7
  from pathlib import Path
7
8
  from textwrap import dedent
8
- from typing import Iterator
9
9
  from unittest import mock
10
10
 
11
11
  import pytest
@@ -5,7 +5,9 @@ import contextlib
5
5
  import logging
6
6
  import signal
7
7
  import sys
8
- from typing import Callable, ContextManager, Generator
8
+ from collections.abc import Generator
9
+ from contextlib import AbstractContextManager
10
+ from typing import Callable
9
11
 
10
12
  import httpx
11
13
  import pytest
@@ -62,7 +64,7 @@ else: # pragma: py-win32
62
64
  @pytest.mark.parametrize("exception_signal", signals)
63
65
  @pytest.mark.parametrize("capture_signal", signal_captures)
64
66
  async def test_server_interrupt(
65
- exception_signal: signal.Signals, capture_signal: Callable[[signal.Signals], ContextManager[None]]
67
+ exception_signal: signal.Signals, capture_signal: Callable[[signal.Signals], AbstractContextManager[None]]
66
68
  ): # pragma: py-win32
67
69
  """Test interrupting a Server that is run explicitly inside asyncio"""
68
70
 
@@ -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.32.1"
4
+ __version__ = "0.34.0"
5
5
  __all__ = ["main", "run", "Config", "Server"]
@@ -32,20 +32,8 @@ from __future__ import annotations
32
32
 
33
33
  import sys
34
34
  import types
35
- from typing import (
36
- Any,
37
- Awaitable,
38
- Callable,
39
- Iterable,
40
- Literal,
41
- MutableMapping,
42
- Optional,
43
- Protocol,
44
- Tuple,
45
- Type,
46
- TypedDict,
47
- Union,
48
- )
35
+ from collections.abc import Awaitable, Iterable, MutableMapping
36
+ from typing import Any, Callable, Literal, Optional, Protocol, TypedDict, Union
49
37
 
50
38
  if sys.version_info >= (3, 11): # pragma: py-lt-311
51
39
  from typing import NotRequired
@@ -54,8 +42,8 @@ else: # pragma: py-gte-311
54
42
 
55
43
  # WSGI
56
44
  Environ = MutableMapping[str, Any]
57
- ExcInfo = Tuple[Type[BaseException], BaseException, Optional[types.TracebackType]]
58
- StartResponse = Callable[[str, Iterable[Tuple[str, str]], Optional[ExcInfo]], None]
45
+ ExcInfo = tuple[type[BaseException], BaseException, Optional[types.TracebackType]]
46
+ StartResponse = Callable[[str, Iterable[tuple[str, str]], Optional[ExcInfo]], None]
59
47
  WSGIApp = Callable[[Environ, StartResponse], Union[Iterable[bytes], BaseException]]
60
48
 
61
49
 
@@ -281,7 +269,7 @@ class ASGI2Protocol(Protocol):
281
269
  async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... # pragma: no cover
282
270
 
283
271
 
284
- ASGI2Application = Type[ASGI2Protocol]
272
+ ASGI2Application = type[ASGI2Protocol]
285
273
  ASGI3Application = Callable[
286
274
  [
287
275
  Scope,
@@ -9,9 +9,10 @@ import os
9
9
  import socket
10
10
  import ssl
11
11
  import sys
12
+ from collections.abc import Awaitable
12
13
  from configparser import RawConfigParser
13
14
  from pathlib import Path
14
- from typing import IO, Any, Awaitable, Callable, Literal
15
+ from typing import IO, Any, Callable, Literal
15
16
 
16
17
  import click
17
18
 
@@ -137,7 +138,7 @@ def resolve_reload_patterns(patterns_list: list[str], directories_list: list[str
137
138
  # Special case for the .* pattern, otherwise this would only match
138
139
  # hidden directories which is probably undesired
139
140
  if pattern == ".*":
140
- continue
141
+ continue # pragma: py-darwin
141
142
  patterns.append(pattern)
142
143
  if is_dir(Path(pattern)):
143
144
  directories.append(Path(pattern))
@@ -16,7 +16,7 @@ class ColourizedFormatter(logging.Formatter):
16
16
  A custom log formatter class that:
17
17
 
18
18
  * Outputs the LOG_LEVEL with an appropriate color.
19
- * If a log call includes an `extras={"color_message": ...}` it will be used
19
+ * If a log call includes an `extra={"color_message": ...}` it will be used
20
20
  for formatting the output, instead of the plain text message.
21
21
  """
22
22
 
@@ -6,7 +6,7 @@ import io
6
6
  import sys
7
7
  import warnings
8
8
  from collections import deque
9
- from typing import Iterable
9
+ from collections.abc import Iterable
10
10
 
11
11
  from uvicorn._types import (
12
12
  ASGIReceiveCallable,
@@ -3,7 +3,8 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import http
5
5
  import logging
6
- from typing import Any, Literal, Optional, Sequence, cast
6
+ from collections.abc import Sequence
7
+ from typing import Any, Literal, Optional, cast
7
8
  from urllib.parse import unquote
8
9
 
9
10
  import websockets
@@ -224,6 +224,7 @@ class WSProtocol(asyncio.Protocol):
224
224
  headers: list[tuple[bytes, bytes]] = [
225
225
  (b"content-type", b"text/plain; charset=utf-8"),
226
226
  (b"connection", b"close"),
227
+ (b"content-length", b"21"),
227
228
  ]
228
229
  output = self.conn.send(wsproto.events.RejectConnection(status_code=500, headers=headers, has_body=True))
229
230
  output += self.conn.send(wsproto.events.RejectData(data=b"Internal Server Error"))
@@ -10,9 +10,10 @@ import socket
10
10
  import sys
11
11
  import threading
12
12
  import time
13
+ from collections.abc import Generator, Sequence
13
14
  from email.utils import formatdate
14
15
  from types import FrameType
15
- from typing import TYPE_CHECKING, Generator, Sequence, Union
16
+ from typing import TYPE_CHECKING, Union
16
17
 
17
18
  import click
18
19
 
@@ -284,10 +285,7 @@ class Server:
284
285
  len(self.server_state.tasks),
285
286
  )
286
287
  for t in self.server_state.tasks:
287
- if sys.version_info < (3, 9): # pragma: py-gte-39
288
- t.cancel()
289
- else: # pragma: py-lt-39
290
- t.cancel(msg="Task cancelled, timeout graceful shutdown exceeded")
288
+ t.cancel(msg="Task cancelled, timeout graceful shutdown exceeded")
291
289
 
292
290
  # Send the lifespan shutdown event, and wait for application shutdown.
293
291
  if not self.force_exit:
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from uvicorn.supervisors.basereload import BaseReload
6
+ from uvicorn.supervisors.multiprocess import Multiprocess
7
+
8
+ if TYPE_CHECKING:
9
+ ChangeReload: type[BaseReload]
10
+ else:
11
+ try:
12
+ from uvicorn.supervisors.watchfilesreload import WatchFilesReload as ChangeReload
13
+ except ImportError: # pragma: no cover
14
+ from uvicorn.supervisors.statreload import StatReload as ChangeReload
15
+
16
+ __all__ = ["Multiprocess", "ChangeReload"]
@@ -5,10 +5,11 @@ import os
5
5
  import signal
6
6
  import sys
7
7
  import threading
8
+ from collections.abc import Iterator
8
9
  from pathlib import Path
9
10
  from socket import socket
10
11
  from types import FrameType
11
- from typing import Callable, Iterator
12
+ from typing import Callable
12
13
 
13
14
  import click
14
15
 
@@ -1,9 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
+ from collections.abc import Iterator
4
5
  from pathlib import Path
5
6
  from socket import socket
6
- from typing import Callable, Iterator
7
+ from typing import Callable
7
8
 
8
9
  from uvicorn.config import Config
9
10
  from uvicorn.supervisors.basereload import BaseReload
@@ -11,7 +11,7 @@ from gunicorn.arbiter import Arbiter
11
11
  from gunicorn.workers.base import Worker
12
12
 
13
13
  from uvicorn.config import Config
14
- from uvicorn.main import Server
14
+ from uvicorn.server import Server
15
15
 
16
16
  warnings.warn(
17
17
  "The `uvicorn.workers` module is deprecated. Please use `uvicorn-worker` package instead.\n"
@@ -1,23 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING
4
-
5
- from uvicorn.supervisors.basereload import BaseReload
6
- from uvicorn.supervisors.multiprocess import Multiprocess
7
-
8
- if TYPE_CHECKING:
9
- ChangeReload: type[BaseReload]
10
- else:
11
- try:
12
- from uvicorn.supervisors.watchfilesreload import (
13
- WatchFilesReload as ChangeReload,
14
- )
15
- except ImportError: # pragma: no cover
16
- try:
17
- from uvicorn.supervisors.watchgodreload import (
18
- WatchGodReload as ChangeReload,
19
- )
20
- except ImportError:
21
- from uvicorn.supervisors.statreload import StatReload as ChangeReload
22
-
23
- __all__ = ["Multiprocess", "ChangeReload"]
@@ -1,152 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import logging
4
- import warnings
5
- from pathlib import Path
6
- from socket import socket
7
- from typing import TYPE_CHECKING, Callable
8
-
9
- from watchgod import DefaultWatcher
10
-
11
- from uvicorn.config import Config
12
- from uvicorn.supervisors.basereload import BaseReload
13
-
14
- if TYPE_CHECKING:
15
- import os
16
-
17
- DirEntry = os.DirEntry[str]
18
-
19
- logger = logging.getLogger("uvicorn.error")
20
-
21
-
22
- class CustomWatcher(DefaultWatcher):
23
- def __init__(self, root_path: Path, config: Config):
24
- default_includes = ["*.py"]
25
- self.includes = [default for default in default_includes if default not in config.reload_excludes]
26
- self.includes.extend(config.reload_includes)
27
- self.includes = list(set(self.includes))
28
-
29
- default_excludes = [".*", ".py[cod]", ".sw.*", "~*"]
30
- self.excludes = [default for default in default_excludes if default not in config.reload_includes]
31
- self.excludes.extend(config.reload_excludes)
32
- self.excludes = list(set(self.excludes))
33
-
34
- self.watched_dirs: dict[str, bool] = {}
35
- self.watched_files: dict[str, bool] = {}
36
- self.dirs_includes = set(config.reload_dirs)
37
- self.dirs_excludes = set(config.reload_dirs_excludes)
38
- self.resolved_root = root_path
39
- super().__init__(str(root_path))
40
-
41
- def should_watch_file(self, entry: DirEntry) -> bool:
42
- cached_result = self.watched_files.get(entry.path)
43
- if cached_result is not None:
44
- return cached_result
45
-
46
- entry_path = Path(entry)
47
-
48
- # cwd is not verified through should_watch_dir, so we need to verify here
49
- if entry_path.parent == Path.cwd() and Path.cwd() not in self.dirs_includes:
50
- self.watched_files[entry.path] = False
51
- return False
52
- for include_pattern in self.includes:
53
- if str(entry_path).endswith(include_pattern):
54
- self.watched_files[entry.path] = True
55
- return True
56
- if entry_path.match(include_pattern):
57
- for exclude_pattern in self.excludes:
58
- if entry_path.match(exclude_pattern):
59
- self.watched_files[entry.path] = False
60
- return False
61
- self.watched_files[entry.path] = True
62
- return True
63
- self.watched_files[entry.path] = False
64
- return False
65
-
66
- def should_watch_dir(self, entry: DirEntry) -> bool:
67
- cached_result = self.watched_dirs.get(entry.path)
68
- if cached_result is not None:
69
- return cached_result
70
-
71
- entry_path = Path(entry)
72
-
73
- if entry_path in self.dirs_excludes:
74
- self.watched_dirs[entry.path] = False
75
- return False
76
-
77
- for exclude_pattern in self.excludes:
78
- if entry_path.match(exclude_pattern):
79
- is_watched = False
80
- if entry_path in self.dirs_includes:
81
- is_watched = True
82
-
83
- for directory in self.dirs_includes:
84
- if directory in entry_path.parents:
85
- is_watched = True
86
-
87
- if is_watched:
88
- logger.debug(
89
- "WatchGodReload detected a new excluded dir '%s' in '%s'; " "Adding to exclude list.",
90
- entry_path.relative_to(self.resolved_root),
91
- str(self.resolved_root),
92
- )
93
- self.watched_dirs[entry.path] = False
94
- self.dirs_excludes.add(entry_path)
95
- return False
96
-
97
- if entry_path in self.dirs_includes:
98
- self.watched_dirs[entry.path] = True
99
- return True
100
-
101
- for directory in self.dirs_includes:
102
- if directory in entry_path.parents:
103
- self.watched_dirs[entry.path] = True
104
- return True
105
-
106
- for include_pattern in self.includes:
107
- if entry_path.match(include_pattern):
108
- logger.info(
109
- "WatchGodReload detected a new reload dir '%s' in '%s'; " "Adding to watch list.",
110
- str(entry_path.relative_to(self.resolved_root)),
111
- str(self.resolved_root),
112
- )
113
- self.dirs_includes.add(entry_path)
114
- self.watched_dirs[entry.path] = True
115
- return True
116
-
117
- self.watched_dirs[entry.path] = False
118
- return False
119
-
120
-
121
- class WatchGodReload(BaseReload):
122
- def __init__(
123
- self,
124
- config: Config,
125
- target: Callable[[list[socket] | None], None],
126
- sockets: list[socket],
127
- ) -> None:
128
- warnings.warn(
129
- '"watchgod" is deprecated, you should switch ' "to watchfiles (`pip install watchfiles`).",
130
- DeprecationWarning,
131
- )
132
- super().__init__(config, target, sockets)
133
- self.reloader_name = "WatchGod"
134
- self.watchers = []
135
- reload_dirs = []
136
- for directory in config.reload_dirs:
137
- if Path.cwd() not in directory.parents:
138
- reload_dirs.append(directory)
139
- if Path.cwd() not in reload_dirs:
140
- reload_dirs.append(Path.cwd())
141
- for w in reload_dirs:
142
- self.watchers.append(CustomWatcher(w.resolve(), self.config))
143
-
144
- def should_restart(self) -> list[Path] | None:
145
- self.pause()
146
-
147
- for watcher in self.watchers:
148
- change = watcher.check()
149
- if change != set():
150
- return list({Path(c[1]) for c in change})
151
-
152
- return None
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