uvicorn 0.32.0__tar.gz → 0.33.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.0 → uvicorn-0.33.0}/PKG-INFO +3 -4
  2. {uvicorn-0.32.0 → uvicorn-0.33.0}/pyproject.toml +4 -9
  3. {uvicorn-0.32.0 → uvicorn-0.33.0}/requirements.txt +12 -11
  4. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/conftest.py +0 -23
  5. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/middleware/test_wsgi.py +1 -1
  6. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/protocols/test_http.py +2 -2
  7. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/supervisors/test_reload.py +58 -90
  8. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/__init__.py +1 -1
  9. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/config.py +1 -1
  10. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/logging.py +1 -1
  11. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/protocols/http/h11_impl.py +1 -4
  12. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/protocols/http/httptools_impl.py +9 -1
  13. uvicorn-0.33.0/uvicorn/supervisors/__init__.py +16 -0
  14. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/workers.py +1 -1
  15. uvicorn-0.32.0/uvicorn/supervisors/__init__.py +0 -23
  16. uvicorn-0.32.0/uvicorn/supervisors/watchgodreload.py +0 -152
  17. {uvicorn-0.32.0 → uvicorn-0.33.0}/.gitignore +0 -0
  18. {uvicorn-0.32.0 → uvicorn-0.33.0}/LICENSE.md +0 -0
  19. {uvicorn-0.32.0 → uvicorn-0.33.0}/README.md +0 -0
  20. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/__init__.py +0 -0
  21. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/importer/__init__.py +0 -0
  22. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/importer/circular_import_a.py +0 -0
  23. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/importer/circular_import_b.py +0 -0
  24. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/importer/raise_import_error.py +0 -0
  25. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/importer/test_importer.py +0 -0
  26. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/middleware/__init__.py +0 -0
  27. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/middleware/test_logging.py +0 -0
  28. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/middleware/test_message_logger.py +0 -0
  29. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/middleware/test_proxy_headers.py +0 -0
  30. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/protocols/__init__.py +0 -0
  31. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/protocols/test_utils.py +0 -0
  32. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/protocols/test_websocket.py +0 -0
  33. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/response.py +0 -0
  34. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/supervisors/__init__.py +0 -0
  35. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/supervisors/test_multiprocess.py +0 -0
  36. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/supervisors/test_signal.py +0 -0
  37. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/test_auto_detection.py +0 -0
  38. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/test_cli.py +0 -0
  39. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/test_config.py +0 -0
  40. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/test_default_headers.py +0 -0
  41. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/test_lifespan.py +0 -0
  42. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/test_main.py +0 -0
  43. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/test_server.py +0 -0
  44. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/test_ssl.py +0 -0
  45. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/test_subprocess.py +0 -0
  46. {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/utils.py +0 -0
  47. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/__main__.py +0 -0
  48. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/_subprocess.py +0 -0
  49. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/_types.py +0 -0
  50. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/importer.py +0 -0
  51. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/lifespan/__init__.py +0 -0
  52. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/lifespan/off.py +0 -0
  53. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/lifespan/on.py +0 -0
  54. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/loops/__init__.py +0 -0
  55. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/loops/asyncio.py +0 -0
  56. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/loops/auto.py +0 -0
  57. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/loops/uvloop.py +0 -0
  58. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/main.py +0 -0
  59. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/middleware/__init__.py +0 -0
  60. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/middleware/asgi2.py +0 -0
  61. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/middleware/message_logger.py +0 -0
  62. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/middleware/proxy_headers.py +0 -0
  63. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/middleware/wsgi.py +0 -0
  64. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/protocols/__init__.py +0 -0
  65. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/protocols/http/__init__.py +0 -0
  66. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/protocols/http/auto.py +0 -0
  67. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/protocols/http/flow_control.py +0 -0
  68. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/protocols/utils.py +0 -0
  69. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/protocols/websockets/__init__.py +0 -0
  70. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/protocols/websockets/auto.py +0 -0
  71. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/protocols/websockets/websockets_impl.py +0 -0
  72. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/protocols/websockets/wsproto_impl.py +0 -0
  73. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/py.typed +0 -0
  74. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/server.py +0 -0
  75. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/supervisors/basereload.py +0 -0
  76. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/supervisors/multiprocess.py +0 -0
  77. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/supervisors/statreload.py +0 -0
  78. {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/supervisors/watchfilesreload.py +0 -0
@@ -1,14 +1,13 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: uvicorn
3
- Version: 0.32.0
3
+ Version: 0.33.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
7
7
  Project-URL: Homepage, https://www.uvicorn.org/
8
8
  Project-URL: Source, https://github.com/encode/uvicorn
9
9
  Author-email: Tom Christie <tom@tomchristie.com>, Marcelo Trylesinski <marcelotryle@gmail.com>
10
- License-Expression: BSD-3-Clause
11
- License-File: LICENSE.md
10
+ License: BSD-3-Clause
12
11
  Classifier: Development Status :: 4 - Beta
13
12
  Classifier: Environment :: Web Environment
14
13
  Classifier: Intended Audience :: Developers
@@ -30,7 +29,7 @@ Requires-Dist: h11>=0.8
30
29
  Requires-Dist: typing-extensions>=4.0; python_version < '3.11'
31
30
  Provides-Extra: standard
32
31
  Requires-Dist: colorama>=0.4; (sys_platform == 'win32') and extra == 'standard'
33
- Requires-Dist: httptools>=0.5.0; extra == 'standard'
32
+ Requires-Dist: httptools>=0.6.3; extra == 'standard'
34
33
  Requires-Dist: python-dotenv>=0.13; extra == 'standard'
35
34
  Requires-Dist: pyyaml>=5.1; extra == 'standard'
36
35
  Requires-Dist: uvloop!=0.15.0,!=0.15.1,>=0.14.0; (sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy')) and extra == 'standard'
@@ -11,7 +11,7 @@ license = "BSD-3-Clause"
11
11
  requires-python = ">=3.8"
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",
@@ -39,7 +39,7 @@ dependencies = [
39
39
  [project.optional-dependencies]
40
40
  standard = [
41
41
  "colorama>=0.4;sys_platform == 'win32'",
42
- "httptools>=0.5.0",
42
+ "httptools>=0.6.3",
43
43
  "python-dotenv>=0.13",
44
44
  "PyYAML>=5.1",
45
45
  "uvloop>=0.14.0,!=0.15.0,!=0.15.1; sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy')",
@@ -60,11 +60,7 @@ Source = "https://github.com/encode/uvicorn"
60
60
  path = "uvicorn/__init__.py"
61
61
 
62
62
  [tool.hatch.build.targets.sdist]
63
- include = [
64
- "/uvicorn",
65
- "/tests",
66
- "/requirements.txt",
67
- ]
63
+ include = ["/uvicorn", "/tests", "/requirements.txt"]
68
64
 
69
65
  [tool.ruff]
70
66
  line-length = 120
@@ -94,10 +90,9 @@ addopts = "-rxXs --strict-config --strict-markers"
94
90
  xfail_strict = true
95
91
  filterwarnings = [
96
92
  "error",
97
- 'ignore: \"watchgod\" is deprecated\, you should switch to watchfiles \(`pip install watchfiles`\)\.:DeprecationWarning',
98
93
  "ignore:Uvicorn's native WSGI implementation is deprecated.*:DeprecationWarning",
99
94
  "ignore: 'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning",
100
- "ignore: remove second argument of ws_handler:DeprecationWarning:websockets"
95
+ "ignore: remove second argument of ws_handler:DeprecationWarning:websockets",
101
96
  ]
102
97
 
103
98
  [tool.coverage.run]
@@ -10,23 +10,24 @@ 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.1.0; python_version < '3.9'
24
+ trustme==1.2.0; python_version >= '3.9'
25
+ cryptography==44.0.0
26
+ coverage==7.6.1; python_version < '3.9'
27
+ coverage==7.6.9; python_version >= '3.9'
26
28
  coverage-conditional-plugin==0.9.0
27
- httpx==0.27.2
28
- watchgod==0.8.2
29
+ httpx==0.28.1
29
30
 
30
31
  # Documentation
31
32
  mkdocs==1.6.1
32
- mkdocs-material==9.5.39
33
+ 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:
@@ -72,7 +72,7 @@ async def test_wsgi_post(wsgi_middleware: Callable) -> None:
72
72
  async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
73
73
  response = await client.post("/", json={"example": 123})
74
74
  assert response.status_code == 200
75
- assert response.text == '{"example": 123}'
75
+ assert response.text == '{"example":123}'
76
76
 
77
77
 
78
78
  @pytest.mark.anyio
@@ -860,8 +860,8 @@ def asgi2app(scope: Scope):
860
860
  @pytest.mark.parametrize(
861
861
  "asgi2or3_app, expected_scopes",
862
862
  [
863
- (asgi3app, {"version": "3.0", "spec_version": "2.4"}),
864
- (asgi2app, {"version": "2.0", "spec_version": "2.4"}),
863
+ (asgi3app, {"version": "3.0", "spec_version": "2.3"}),
864
+ (asgi2app, {"version": "2.0", "spec_version": "2.3"}),
865
865
  ],
866
866
  )
867
867
  async def test_scopes(
@@ -1,14 +1,16 @@
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
8
7
  from pathlib import Path
8
+ from threading import Thread
9
9
  from time import sleep
10
+ from typing import Callable, Generator
10
11
 
11
12
  import pytest
13
+ from pytest_mock import MockerFixture
12
14
 
13
15
  from tests.utils import as_cwd
14
16
  from uvicorn.config import Config
@@ -20,11 +22,6 @@ try:
20
22
  except ImportError: # pragma: no cover
21
23
  WatchFilesReload = None # type: ignore[misc,assignment]
22
24
 
23
- try:
24
- from uvicorn.supervisors.watchgodreload import WatchGodReload
25
- except ImportError: # pragma: no cover
26
- WatchGodReload = None # type: ignore[misc,assignment]
27
-
28
25
 
29
26
  # TODO: Investigate why this is flaky on MacOS M1.
30
27
  skip_if_m1 = pytest.mark.skipif(
@@ -33,17 +30,34 @@ skip_if_m1 = pytest.mark.skipif(
33
30
  )
34
31
 
35
32
 
36
- def run(sockets):
33
+ def run(sockets: list[socket.socket] | None) -> None:
37
34
  pass # pragma: no cover
38
35
 
39
36
 
37
+ def sleep_touch(*paths: Path):
38
+ sleep(0.1)
39
+ for p in paths:
40
+ p.touch()
41
+
42
+
43
+ @pytest.fixture
44
+ def touch_soon() -> Generator[Callable[[Path], None]]:
45
+ threads: list[Thread] = []
46
+
47
+ def start(*paths: Path) -> None:
48
+ thread = Thread(target=sleep_touch, args=paths)
49
+ thread.start()
50
+ threads.append(thread)
51
+
52
+ yield start
53
+
54
+ for t in threads:
55
+ t.join()
56
+
57
+
40
58
  class TestBaseReload:
41
59
  @pytest.fixture(autouse=True)
42
- def setup(
43
- self,
44
- reload_directory_structure: Path,
45
- reloader_class: type[BaseReload] | None,
46
- ):
60
+ def setup(self, reload_directory_structure: Path, reloader_class: type[BaseReload] | None):
47
61
  if reloader_class is None: # pragma: no cover
48
62
  pytest.skip("Needed dependency not installed")
49
63
  self.reload_path = reload_directory_structure
@@ -52,17 +66,15 @@ class TestBaseReload:
52
66
  def _setup_reloader(self, config: Config) -> BaseReload:
53
67
  config.reload_delay = 0 # save time
54
68
 
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=[])
69
+ reloader = self.reloader_class(config, target=run, sockets=[])
60
70
 
61
71
  assert config.should_reload
62
72
  reloader.startup()
63
73
  return reloader
64
74
 
65
- def _reload_tester(self, touch_soon, reloader: BaseReload, *files: Path) -> list[Path] | None:
75
+ def _reload_tester(
76
+ self, touch_soon: Callable[[Path], None], reloader: BaseReload, *files: Path
77
+ ) -> list[Path] | None:
66
78
  reloader.restart()
67
79
  if WatchFilesReload is not None and isinstance(reloader, WatchFilesReload):
68
80
  touch_soon(*files)
@@ -73,7 +85,7 @@ class TestBaseReload:
73
85
  file.touch()
74
86
  return next(reloader)
75
87
 
76
- @pytest.mark.parametrize("reloader_class", [StatReload, WatchGodReload, WatchFilesReload])
88
+ @pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
77
89
  def test_reloader_should_initialize(self) -> None:
78
90
  """
79
91
  A basic sanity check.
@@ -86,8 +98,8 @@ class TestBaseReload:
86
98
  reloader = self._setup_reloader(config)
87
99
  reloader.shutdown()
88
100
 
89
- @pytest.mark.parametrize("reloader_class", [StatReload, WatchGodReload, WatchFilesReload])
90
- def test_reload_when_python_file_is_changed(self, touch_soon) -> None:
101
+ @pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
102
+ def test_reload_when_python_file_is_changed(self, touch_soon: Callable[[Path], None]):
91
103
  file = self.reload_path / "main.py"
92
104
 
93
105
  with as_cwd(self.reload_path):
@@ -99,8 +111,8 @@ class TestBaseReload:
99
111
 
100
112
  reloader.shutdown()
101
113
 
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:
114
+ @pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
115
+ def test_should_reload_when_python_file_in_subdir_is_changed(self, touch_soon: Callable[[Path], None]):
104
116
  file = self.reload_path / "app" / "sub" / "sub.py"
105
117
 
106
118
  with as_cwd(self.reload_path):
@@ -111,8 +123,8 @@ class TestBaseReload:
111
123
 
112
124
  reloader.shutdown()
113
125
 
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:
126
+ @pytest.mark.parametrize("reloader_class", [WatchFilesReload])
127
+ def test_should_not_reload_when_python_file_in_excluded_subdir_is_changed(self, touch_soon: Callable[[Path], None]):
116
128
  sub_dir = self.reload_path / "app" / "sub"
117
129
  sub_file = sub_dir / "sub.py"
118
130
 
@@ -129,7 +141,7 @@ class TestBaseReload:
129
141
  reloader.shutdown()
130
142
 
131
143
  @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:
144
+ def test_reload_when_pattern_matched_file_is_changed(self, result: bool, touch_soon: Callable[[Path], None]):
133
145
  file = self.reload_path / "app" / "js" / "main.js"
134
146
 
135
147
  with as_cwd(self.reload_path):
@@ -140,14 +152,10 @@ class TestBaseReload:
140
152
 
141
153
  reloader.shutdown()
142
154
 
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:
155
+ @pytest.mark.parametrize("reloader_class", [pytest.param(WatchFilesReload, marks=skip_if_m1)])
156
+ def test_should_not_reload_when_exclude_pattern_match_file_is_changed(
157
+ self, touch_soon: Callable[[Path], None]
158
+ ): # pragma: py-darwin
151
159
  python_file = self.reload_path / "app" / "src" / "main.py"
152
160
  css_file = self.reload_path / "app" / "css" / "main.css"
153
161
  js_file = self.reload_path / "app" / "js" / "main.js"
@@ -167,8 +175,8 @@ class TestBaseReload:
167
175
 
168
176
  reloader.shutdown()
169
177
 
170
- @pytest.mark.parametrize("reloader_class", [StatReload, WatchGodReload, WatchFilesReload])
171
- def test_should_not_reload_when_dot_file_is_changed(self, touch_soon) -> None:
178
+ @pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
179
+ def test_should_not_reload_when_dot_file_is_changed(self, touch_soon: Callable[[Path], None]):
172
180
  file = self.reload_path / ".dotted"
173
181
 
174
182
  with as_cwd(self.reload_path):
@@ -179,8 +187,8 @@ class TestBaseReload:
179
187
 
180
188
  reloader.shutdown()
181
189
 
182
- @pytest.mark.parametrize("reloader_class", [StatReload, WatchGodReload, WatchFilesReload])
183
- def test_should_reload_when_directories_have_same_prefix(self, touch_soon) -> None:
190
+ @pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
191
+ def test_should_reload_when_directories_have_same_prefix(self, touch_soon: Callable[[Path], None]):
184
192
  app_dir = self.reload_path / "app"
185
193
  app_file = app_dir / "src" / "main.py"
186
194
  app_first_dir = self.reload_path / "app_first"
@@ -201,13 +209,9 @@ class TestBaseReload:
201
209
 
202
210
  @pytest.mark.parametrize(
203
211
  "reloader_class",
204
- [
205
- StatReload,
206
- WatchGodReload,
207
- pytest.param(WatchFilesReload, marks=skip_if_m1),
208
- ],
212
+ [StatReload, pytest.param(WatchFilesReload, marks=skip_if_m1)],
209
213
  )
210
- def test_should_not_reload_when_only_subdirectory_is_watched(self, touch_soon) -> None:
214
+ def test_should_not_reload_when_only_subdirectory_is_watched(self, touch_soon: Callable[[Path], None]):
211
215
  app_dir = self.reload_path / "app"
212
216
  app_dir_file = self.reload_path / "app" / "src" / "main.py"
213
217
  root_file = self.reload_path / "main.py"
@@ -224,14 +228,8 @@ class TestBaseReload:
224
228
 
225
229
  reloader.shutdown()
226
230
 
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:
231
+ @pytest.mark.parametrize("reloader_class", [pytest.param(WatchFilesReload, marks=skip_if_m1)])
232
+ def test_override_defaults(self, touch_soon: Callable[[Path], None]) -> None: # pragma: py-darwin
235
233
  dotted_file = self.reload_path / ".dotted"
236
234
  dotted_dir_file = self.reload_path / ".dotted_dir" / "file.txt"
237
235
  python_file = self.reload_path / "main.py"
@@ -252,14 +250,8 @@ class TestBaseReload:
252
250
 
253
251
  reloader.shutdown()
254
252
 
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:
253
+ @pytest.mark.parametrize("reloader_class", [pytest.param(WatchFilesReload, marks=skip_if_m1)])
254
+ def test_explicit_paths(self, touch_soon: Callable[[Path], None]) -> None: # pragma: py-darwin
263
255
  dotted_file = self.reload_path / ".dotted"
264
256
  non_dotted_file = self.reload_path / "ext" / "ext.jpg"
265
257
  python_file = self.reload_path / "main.py"
@@ -307,33 +299,9 @@ class TestBaseReload:
307
299
 
308
300
  reloader.shutdown()
309
301
 
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
302
 
335
303
  @pytest.mark.skipif(WatchFilesReload is None, reason="watchfiles not available")
336
- def test_should_watch_one_dir_cwd(mocker, reload_directory_structure):
304
+ def test_should_watch_one_dir_cwd(mocker: MockerFixture, reload_directory_structure: Path):
337
305
  mock_watch = mocker.patch("uvicorn.supervisors.watchfilesreload.watch")
338
306
  app_dir = reload_directory_structure / "app"
339
307
  app_first_dir = reload_directory_structure / "app_first"
@@ -350,7 +318,7 @@ def test_should_watch_one_dir_cwd(mocker, reload_directory_structure):
350
318
 
351
319
 
352
320
  @pytest.mark.skipif(WatchFilesReload is None, reason="watchfiles not available")
353
- def test_should_watch_separate_dirs_outside_cwd(mocker, reload_directory_structure):
321
+ def test_should_watch_separate_dirs_outside_cwd(mocker: MockerFixture, reload_directory_structure: Path):
354
322
  mock_watch = mocker.patch("uvicorn.supervisors.watchfilesreload.watch")
355
323
  app_dir = reload_directory_structure / "app"
356
324
  app_first_dir = reload_directory_structure / "app_first"
@@ -368,7 +336,7 @@ def test_should_watch_separate_dirs_outside_cwd(mocker, reload_directory_structu
368
336
  }
369
337
 
370
338
 
371
- def test_display_path_relative(tmp_path):
339
+ def test_display_path_relative(tmp_path: Path):
372
340
  with as_cwd(tmp_path):
373
341
  p = tmp_path / "app" / "foobar.py"
374
342
  # accept windows paths as wells as posix
@@ -380,8 +348,8 @@ def test_display_path_non_relative():
380
348
  assert _display_path(p) in ("'/foo/bar.py'", "'\\foo\\bar.py'")
381
349
 
382
350
 
383
- def test_base_reloader_run(tmp_path):
384
- calls = []
351
+ def test_base_reloader_run(tmp_path: Path):
352
+ calls: list[str] = []
385
353
  step = 0
386
354
 
387
355
  class CustomReload(BaseReload):
@@ -411,7 +379,7 @@ def test_base_reloader_run(tmp_path):
411
379
  assert calls == ["startup", "restart", "shutdown"]
412
380
 
413
381
 
414
- def test_base_reloader_should_exit(tmp_path):
382
+ def test_base_reloader_should_exit(tmp_path: Path):
415
383
  config = Config(app="tests.test_config:asgi_app", reload=True)
416
384
  reloader = BaseReload(config, target=run, sockets=[])
417
385
  assert not reloader.should_exit.is_set()
@@ -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.0"
4
+ __version__ = "0.33.0"
5
5
  __all__ = ["main", "run", "Config", "Server"]
@@ -137,7 +137,7 @@ def resolve_reload_patterns(patterns_list: list[str], directories_list: list[str
137
137
  # Special case for the .* pattern, otherwise this would only match
138
138
  # hidden directories which is probably undesired
139
139
  if pattern == ".*":
140
- continue
140
+ continue # pragma: py-darwin
141
141
  patterns.append(pattern)
142
142
  if is_dir(Path(pattern)):
143
143
  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
 
@@ -200,10 +200,7 @@ class H11Protocol(asyncio.Protocol):
200
200
  full_raw_path = self.root_path.encode("ascii") + raw_path
201
201
  self.scope = {
202
202
  "type": "http",
203
- "asgi": {
204
- "version": self.config.asgi_version,
205
- "spec_version": "2.4",
206
- },
203
+ "asgi": {"version": self.config.asgi_version, "spec_version": "2.3"},
207
204
  "http_version": event.http_version.decode("ascii"),
208
205
  "server": self.server,
209
206
  "client": self.client,
@@ -58,6 +58,14 @@ class HttpToolsProtocol(asyncio.Protocol):
58
58
  self.access_logger = logging.getLogger("uvicorn.access")
59
59
  self.access_log = self.access_logger.hasHandlers()
60
60
  self.parser = httptools.HttpRequestParser(self)
61
+
62
+ try:
63
+ # Enable dangerous leniencies to allow server to a response on the first request from a pipelined request.
64
+ self.parser.set_dangerous_leniencies(lenient_data_after_close=True)
65
+ except AttributeError: # pragma: no cover
66
+ # httptools < 0.6.3
67
+ pass
68
+
61
69
  self.ws_protocol_class = config.ws_protocol_class
62
70
  self.root_path = config.root_path
63
71
  self.limit_concurrency = config.limit_concurrency
@@ -214,7 +222,7 @@ class HttpToolsProtocol(asyncio.Protocol):
214
222
  self.headers = []
215
223
  self.scope = { # type: ignore[typeddict-item]
216
224
  "type": "http",
217
- "asgi": {"version": self.config.asgi_version, "spec_version": "2.4"},
225
+ "asgi": {"version": self.config.asgi_version, "spec_version": "2.3"},
218
226
  "http_version": "1.1",
219
227
  "server": self.server,
220
228
  "client": self.client,
@@ -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"]
@@ -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
File without changes
File without changes
File without changes
File without changes