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.
- {uvicorn-0.32.0 → uvicorn-0.33.0}/PKG-INFO +3 -4
- {uvicorn-0.32.0 → uvicorn-0.33.0}/pyproject.toml +4 -9
- {uvicorn-0.32.0 → uvicorn-0.33.0}/requirements.txt +12 -11
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/conftest.py +0 -23
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/middleware/test_wsgi.py +1 -1
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/protocols/test_http.py +2 -2
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/supervisors/test_reload.py +58 -90
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/__init__.py +1 -1
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/config.py +1 -1
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/logging.py +1 -1
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/protocols/http/h11_impl.py +1 -4
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/protocols/http/httptools_impl.py +9 -1
- uvicorn-0.33.0/uvicorn/supervisors/__init__.py +16 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/workers.py +1 -1
- uvicorn-0.32.0/uvicorn/supervisors/__init__.py +0 -23
- uvicorn-0.32.0/uvicorn/supervisors/watchgodreload.py +0 -152
- {uvicorn-0.32.0 → uvicorn-0.33.0}/.gitignore +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/LICENSE.md +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/README.md +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/__init__.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/importer/__init__.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/importer/circular_import_a.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/importer/circular_import_b.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/importer/raise_import_error.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/importer/test_importer.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/middleware/__init__.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/middleware/test_logging.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/middleware/test_message_logger.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/middleware/test_proxy_headers.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/protocols/__init__.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/protocols/test_utils.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/protocols/test_websocket.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/response.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/supervisors/__init__.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/supervisors/test_multiprocess.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/supervisors/test_signal.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/test_auto_detection.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/test_cli.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/test_config.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/test_default_headers.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/test_lifespan.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/test_main.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/test_server.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/test_ssl.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/test_subprocess.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/tests/utils.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/__main__.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/_subprocess.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/_types.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/importer.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/lifespan/__init__.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/lifespan/off.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/lifespan/on.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/loops/__init__.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/loops/asyncio.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/loops/auto.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/loops/uvloop.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/main.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/middleware/__init__.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/middleware/asgi2.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/middleware/message_logger.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/middleware/proxy_headers.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/middleware/wsgi.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/protocols/__init__.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/protocols/http/__init__.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/protocols/http/auto.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/protocols/http/flow_control.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/protocols/utils.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/protocols/websockets/__init__.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/protocols/websockets/auto.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/protocols/websockets/websockets_impl.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/protocols/websockets/wsproto_impl.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/py.typed +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/server.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/supervisors/basereload.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/supervisors/multiprocess.py +0 -0
- {uvicorn-0.32.0 → uvicorn-0.33.0}/uvicorn/supervisors/statreload.py +0 -0
- {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.
|
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
|
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.
|
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.
|
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==
|
13
|
+
build==1.2.2.post1
|
14
|
+
twine==6.0.1
|
15
15
|
|
16
16
|
# Testing
|
17
|
-
ruff==0.
|
18
|
-
pytest==8.3.
|
17
|
+
ruff==0.8.3
|
18
|
+
pytest==8.3.4
|
19
19
|
pytest-mock==3.14.0
|
20
|
-
mypy==1.
|
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
|
-
|
25
|
-
|
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.
|
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.
|
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":
|
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.
|
864
|
-
(asgi2app, {"version": "2.0", "spec_version": "2.
|
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
|
-
|
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(
|
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,
|
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,
|
90
|
-
def test_reload_when_python_file_is_changed(self, touch_soon
|
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,
|
103
|
-
def test_should_reload_when_python_file_in_subdir_is_changed(self, touch_soon
|
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
|
115
|
-
def test_should_not_reload_when_python_file_in_excluded_subdir_is_changed(self, touch_soon
|
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
|
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
|
-
|
145
|
-
[
|
146
|
-
|
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,
|
171
|
-
def test_should_not_reload_when_dot_file_is_changed(self, touch_soon
|
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,
|
183
|
-
def test_should_reload_when_directories_have_same_prefix(self, touch_soon
|
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
|
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
|
-
|
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
|
-
|
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()
|
@@ -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 `
|
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.
|
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.
|
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
|
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
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|