uvicorn 0.30.6__tar.gz → 0.31.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.30.6 → uvicorn-0.31.0}/PKG-INFO +1 -1
- {uvicorn-0.30.6 → uvicorn-0.31.0}/pyproject.toml +5 -1
- uvicorn-0.31.0/requirements.txt +32 -0
- uvicorn-0.31.0/tests/conftest.py +283 -0
- uvicorn-0.31.0/tests/importer/circular_import_a.py +4 -0
- uvicorn-0.31.0/tests/importer/circular_import_b.py +4 -0
- uvicorn-0.31.0/tests/importer/raise_import_error.py +5 -0
- uvicorn-0.31.0/tests/importer/test_importer.py +53 -0
- uvicorn-0.31.0/tests/middleware/test_logging.py +211 -0
- uvicorn-0.31.0/tests/middleware/test_message_logger.py +49 -0
- uvicorn-0.31.0/tests/middleware/test_proxy_headers.py +490 -0
- uvicorn-0.31.0/tests/middleware/test_wsgi.py +138 -0
- uvicorn-0.31.0/tests/protocols/test_http.py +1126 -0
- uvicorn-0.31.0/tests/protocols/test_utils.py +82 -0
- uvicorn-0.31.0/tests/protocols/test_websocket.py +1416 -0
- uvicorn-0.31.0/tests/response.py +37 -0
- uvicorn-0.31.0/tests/supervisors/test_multiprocess.py +171 -0
- uvicorn-0.31.0/tests/supervisors/test_reload.py +437 -0
- uvicorn-0.31.0/tests/supervisors/test_signal.py +104 -0
- uvicorn-0.31.0/tests/test_auto_detection.py +59 -0
- uvicorn-0.31.0/tests/test_cli.py +203 -0
- uvicorn-0.31.0/tests/test_config.py +547 -0
- uvicorn-0.31.0/tests/test_default_headers.py +69 -0
- uvicorn-0.31.0/tests/test_lifespan.py +264 -0
- uvicorn-0.31.0/tests/test_main.py +115 -0
- uvicorn-0.31.0/tests/test_server.py +67 -0
- uvicorn-0.31.0/tests/test_ssl.py +94 -0
- uvicorn-0.31.0/tests/test_subprocess.py +43 -0
- uvicorn-0.31.0/tests/utils.py +46 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/__init__.py +1 -1
- uvicorn-0.31.0/uvicorn/loops/__init__.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/main.py +4 -2
- uvicorn-0.31.0/uvicorn/middleware/__init__.py +0 -0
- uvicorn-0.31.0/uvicorn/middleware/proxy_headers.py +142 -0
- uvicorn-0.31.0/uvicorn/protocols/__init__.py +0 -0
- uvicorn-0.31.0/uvicorn/protocols/http/__init__.py +0 -0
- uvicorn-0.31.0/uvicorn/protocols/websockets/__init__.py +0 -0
- uvicorn-0.30.6/uvicorn/middleware/proxy_headers.py +0 -70
- {uvicorn-0.30.6 → uvicorn-0.31.0}/.gitignore +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/LICENSE.md +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/README.md +0 -0
- {uvicorn-0.30.6/uvicorn/lifespan → uvicorn-0.31.0/tests}/__init__.py +0 -0
- {uvicorn-0.30.6/uvicorn/loops → uvicorn-0.31.0/tests/importer}/__init__.py +0 -0
- {uvicorn-0.30.6/uvicorn → uvicorn-0.31.0/tests}/middleware/__init__.py +0 -0
- {uvicorn-0.30.6/uvicorn → uvicorn-0.31.0/tests}/protocols/__init__.py +0 -0
- {uvicorn-0.30.6/uvicorn/protocols/http → uvicorn-0.31.0/tests/supervisors}/__init__.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/__main__.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/_subprocess.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/_types.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/config.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/importer.py +0 -0
- {uvicorn-0.30.6/uvicorn/protocols/websockets → uvicorn-0.31.0/uvicorn/lifespan}/__init__.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/lifespan/off.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/lifespan/on.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/logging.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/loops/asyncio.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/loops/auto.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/loops/uvloop.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/middleware/asgi2.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/middleware/message_logger.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/middleware/wsgi.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/protocols/http/auto.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/protocols/http/flow_control.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/protocols/http/h11_impl.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/protocols/http/httptools_impl.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/protocols/utils.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/protocols/websockets/auto.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/protocols/websockets/websockets_impl.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/protocols/websockets/wsproto_impl.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/py.typed +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/server.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/supervisors/__init__.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/supervisors/basereload.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/supervisors/multiprocess.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/supervisors/statreload.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/supervisors/watchfilesreload.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/supervisors/watchgodreload.py +0 -0
- {uvicorn-0.30.6 → uvicorn-0.31.0}/uvicorn/workers.py +0 -0
@@ -59,7 +59,11 @@ Source = "https://github.com/encode/uvicorn"
|
|
59
59
|
path = "uvicorn/__init__.py"
|
60
60
|
|
61
61
|
[tool.hatch.build.targets.sdist]
|
62
|
-
include = [
|
62
|
+
include = [
|
63
|
+
"/uvicorn",
|
64
|
+
"/tests",
|
65
|
+
"/requirements.txt",
|
66
|
+
]
|
63
67
|
|
64
68
|
[tool.ruff]
|
65
69
|
line-length = 120
|
@@ -0,0 +1,32 @@
|
|
1
|
+
-e .[standard]
|
2
|
+
|
3
|
+
# TODO: Remove this after h11 makes a release. By this writing, h11 was on 0.14.0.
|
4
|
+
# Core dependencies
|
5
|
+
h11 @ git+https://github.com/python-hyper/h11.git@master
|
6
|
+
|
7
|
+
# Explicit optionals
|
8
|
+
a2wsgi==1.10.6
|
9
|
+
wsproto==1.2.0
|
10
|
+
websockets==12.0
|
11
|
+
|
12
|
+
# Packaging
|
13
|
+
build==1.2.1
|
14
|
+
twine==5.1.1
|
15
|
+
|
16
|
+
# Testing
|
17
|
+
ruff==0.5.0
|
18
|
+
pytest==8.2.2
|
19
|
+
pytest-mock==3.14.0
|
20
|
+
mypy==1.10.1
|
21
|
+
types-click==7.1.8
|
22
|
+
types-pyyaml==6.0.12.20240311
|
23
|
+
trustme==1.1.0
|
24
|
+
cryptography==43.0.1
|
25
|
+
coverage==7.5.4
|
26
|
+
coverage-conditional-plugin==0.9.0
|
27
|
+
httpx==0.27.0
|
28
|
+
watchgod==0.8.2
|
29
|
+
|
30
|
+
# Documentation
|
31
|
+
mkdocs==1.6.0
|
32
|
+
mkdocs-material==9.5.27
|
@@ -0,0 +1,283 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import contextlib
|
4
|
+
import importlib.util
|
5
|
+
import os
|
6
|
+
import socket
|
7
|
+
import ssl
|
8
|
+
from copy import deepcopy
|
9
|
+
from hashlib import md5
|
10
|
+
from pathlib import Path
|
11
|
+
from tempfile import TemporaryDirectory
|
12
|
+
from threading import Thread
|
13
|
+
from time import sleep
|
14
|
+
from typing import Any
|
15
|
+
from uuid import uuid4
|
16
|
+
|
17
|
+
import pytest
|
18
|
+
|
19
|
+
try:
|
20
|
+
import trustme
|
21
|
+
from cryptography.hazmat.backends import default_backend
|
22
|
+
from cryptography.hazmat.primitives import serialization
|
23
|
+
|
24
|
+
HAVE_TRUSTME = True
|
25
|
+
except ImportError: # pragma: no cover
|
26
|
+
HAVE_TRUSTME = False
|
27
|
+
|
28
|
+
from uvicorn.config import LOGGING_CONFIG
|
29
|
+
from uvicorn.importer import import_from_string
|
30
|
+
|
31
|
+
# Note: We explicitly turn the propagate on just for tests, because pytest
|
32
|
+
# caplog not able to capture no-propagate loggers.
|
33
|
+
#
|
34
|
+
# And the caplog_for_logger helper also not work on test config cases, because
|
35
|
+
# when create Config object, Config.configure_logging will remove caplog.handler.
|
36
|
+
#
|
37
|
+
# The simple solution is set propagate=True before execute tests.
|
38
|
+
#
|
39
|
+
# See also: https://github.com/pytest-dev/pytest/issues/3697
|
40
|
+
LOGGING_CONFIG["loggers"]["uvicorn"]["propagate"] = True
|
41
|
+
|
42
|
+
|
43
|
+
@pytest.fixture
|
44
|
+
def tls_certificate_authority() -> trustme.CA:
|
45
|
+
if not HAVE_TRUSTME:
|
46
|
+
pytest.skip("trustme not installed") # pragma: no cover
|
47
|
+
return trustme.CA()
|
48
|
+
|
49
|
+
|
50
|
+
@pytest.fixture
|
51
|
+
def tls_certificate(tls_certificate_authority: trustme.CA) -> trustme.LeafCert:
|
52
|
+
return tls_certificate_authority.issue_cert(
|
53
|
+
"localhost",
|
54
|
+
"127.0.0.1",
|
55
|
+
"::1",
|
56
|
+
)
|
57
|
+
|
58
|
+
|
59
|
+
@pytest.fixture
|
60
|
+
def tls_ca_certificate_pem_path(tls_certificate_authority: trustme.CA):
|
61
|
+
with tls_certificate_authority.cert_pem.tempfile() as ca_cert_pem:
|
62
|
+
yield ca_cert_pem
|
63
|
+
|
64
|
+
|
65
|
+
@pytest.fixture
|
66
|
+
def tls_ca_certificate_private_key_path(tls_certificate_authority: trustme.CA):
|
67
|
+
with tls_certificate_authority.private_key_pem.tempfile() as private_key:
|
68
|
+
yield private_key
|
69
|
+
|
70
|
+
|
71
|
+
@pytest.fixture
|
72
|
+
def tls_certificate_private_key_encrypted_path(tls_certificate):
|
73
|
+
private_key = serialization.load_pem_private_key(
|
74
|
+
tls_certificate.private_key_pem.bytes(),
|
75
|
+
password=None,
|
76
|
+
backend=default_backend(),
|
77
|
+
)
|
78
|
+
encrypted_key = private_key.private_bytes(
|
79
|
+
serialization.Encoding.PEM,
|
80
|
+
serialization.PrivateFormat.TraditionalOpenSSL,
|
81
|
+
serialization.BestAvailableEncryption(b"uvicorn password for the win"),
|
82
|
+
)
|
83
|
+
with trustme.Blob(encrypted_key).tempfile() as private_encrypted_key:
|
84
|
+
yield private_encrypted_key
|
85
|
+
|
86
|
+
|
87
|
+
@pytest.fixture
|
88
|
+
def tls_certificate_private_key_path(tls_certificate: trustme.CA):
|
89
|
+
with tls_certificate.private_key_pem.tempfile() as private_key:
|
90
|
+
yield private_key
|
91
|
+
|
92
|
+
|
93
|
+
@pytest.fixture
|
94
|
+
def tls_certificate_key_and_chain_path(tls_certificate: trustme.LeafCert):
|
95
|
+
with tls_certificate.private_key_and_cert_chain_pem.tempfile() as cert_pem:
|
96
|
+
yield cert_pem
|
97
|
+
|
98
|
+
|
99
|
+
@pytest.fixture
|
100
|
+
def tls_certificate_server_cert_path(tls_certificate: trustme.LeafCert):
|
101
|
+
with tls_certificate.cert_chain_pems[0].tempfile() as cert_pem:
|
102
|
+
yield cert_pem
|
103
|
+
|
104
|
+
|
105
|
+
@pytest.fixture
|
106
|
+
def tls_ca_ssl_context(tls_certificate_authority: trustme.CA) -> ssl.SSLContext:
|
107
|
+
ssl_ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
108
|
+
tls_certificate_authority.configure_trust(ssl_ctx)
|
109
|
+
return ssl_ctx
|
110
|
+
|
111
|
+
|
112
|
+
@pytest.fixture(scope="package")
|
113
|
+
def reload_directory_structure(tmp_path_factory: pytest.TempPathFactory):
|
114
|
+
"""
|
115
|
+
This fixture creates a directory structure to enable reload parameter tests
|
116
|
+
|
117
|
+
The fixture has the following structure:
|
118
|
+
root
|
119
|
+
├── [app, app_first, app_second, app_third]
|
120
|
+
│ ├── css
|
121
|
+
│ │ └── main.css
|
122
|
+
│ ├── js
|
123
|
+
│ │ └── main.js
|
124
|
+
│ ├── src
|
125
|
+
│ │ └── main.py
|
126
|
+
│ └── sub
|
127
|
+
│ └── sub.py
|
128
|
+
├── ext
|
129
|
+
│ └── ext.jpg
|
130
|
+
├── .dotted
|
131
|
+
├── .dotted_dir
|
132
|
+
│ └── file.txt
|
133
|
+
└── main.py
|
134
|
+
"""
|
135
|
+
root = tmp_path_factory.mktemp("reload_directory")
|
136
|
+
apps = ["app", "app_first", "app_second", "app_third"]
|
137
|
+
|
138
|
+
root_file = root / "main.py"
|
139
|
+
root_file.touch()
|
140
|
+
|
141
|
+
dotted_file = root / ".dotted"
|
142
|
+
dotted_file.touch()
|
143
|
+
|
144
|
+
dotted_dir = root / ".dotted_dir"
|
145
|
+
dotted_dir.mkdir()
|
146
|
+
dotted_dir_file = dotted_dir / "file.txt"
|
147
|
+
dotted_dir_file.touch()
|
148
|
+
|
149
|
+
for app in apps:
|
150
|
+
app_path = root / app
|
151
|
+
app_path.mkdir()
|
152
|
+
dir_files = [
|
153
|
+
("src", ["main.py"]),
|
154
|
+
("js", ["main.js"]),
|
155
|
+
("css", ["main.css"]),
|
156
|
+
("sub", ["sub.py"]),
|
157
|
+
]
|
158
|
+
for directory, files in dir_files:
|
159
|
+
directory_path = app_path / directory
|
160
|
+
directory_path.mkdir()
|
161
|
+
for file in files:
|
162
|
+
file_path = directory_path / file
|
163
|
+
file_path.touch()
|
164
|
+
ext_dir = root / "ext"
|
165
|
+
ext_dir.mkdir()
|
166
|
+
ext_file = ext_dir / "ext.jpg"
|
167
|
+
ext_file.touch()
|
168
|
+
|
169
|
+
yield root
|
170
|
+
|
171
|
+
|
172
|
+
@pytest.fixture
|
173
|
+
def anyio_backend() -> str:
|
174
|
+
return "asyncio"
|
175
|
+
|
176
|
+
|
177
|
+
@pytest.fixture(scope="function")
|
178
|
+
def logging_config() -> dict[str, Any]:
|
179
|
+
return deepcopy(LOGGING_CONFIG)
|
180
|
+
|
181
|
+
|
182
|
+
@pytest.fixture
|
183
|
+
def short_socket_name(tmp_path, tmp_path_factory): # pragma: py-win32
|
184
|
+
max_sock_len = 100
|
185
|
+
socket_filename = "my.sock"
|
186
|
+
identifier = f"{uuid4()}-"
|
187
|
+
identifier_len = len(identifier.encode())
|
188
|
+
tmp_dir = Path("/tmp").resolve()
|
189
|
+
os_tmp_dir = Path(os.getenv("TMPDIR", "/tmp")).resolve()
|
190
|
+
basetemp = Path(
|
191
|
+
str(tmp_path_factory.getbasetemp()),
|
192
|
+
).resolve()
|
193
|
+
hash_basetemp = md5(
|
194
|
+
str(basetemp).encode(),
|
195
|
+
).hexdigest()
|
196
|
+
|
197
|
+
def make_tmp_dir(base_dir):
|
198
|
+
return TemporaryDirectory(
|
199
|
+
dir=str(base_dir),
|
200
|
+
prefix="p-",
|
201
|
+
suffix=f"-{hash_basetemp}",
|
202
|
+
)
|
203
|
+
|
204
|
+
paths = basetemp, os_tmp_dir, tmp_dir
|
205
|
+
for _num, tmp_dir_path in enumerate(paths, 1):
|
206
|
+
with make_tmp_dir(tmp_dir_path) as tmpd:
|
207
|
+
tmpd = Path(tmpd).resolve()
|
208
|
+
sock_path = str(tmpd / socket_filename)
|
209
|
+
sock_path_len = len(sock_path.encode())
|
210
|
+
if sock_path_len <= max_sock_len:
|
211
|
+
if max_sock_len - sock_path_len >= identifier_len: # pragma: no cover
|
212
|
+
sock_path = str(tmpd / "".join((identifier, socket_filename)))
|
213
|
+
yield sock_path
|
214
|
+
return
|
215
|
+
|
216
|
+
|
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
|
+
def _unused_port(socket_type: int) -> int:
|
239
|
+
"""Find an unused localhost port from 1024-65535 and return it."""
|
240
|
+
with contextlib.closing(socket.socket(type=socket_type)) as sock:
|
241
|
+
sock.bind(("127.0.0.1", 0))
|
242
|
+
return sock.getsockname()[1]
|
243
|
+
|
244
|
+
|
245
|
+
# This was copied from pytest-asyncio.
|
246
|
+
# Ref.: https://github.com/pytest-dev/pytest-asyncio/blob/25d9592286682bc6dbfbf291028ff7a9594cf283/pytest_asyncio/plugin.py#L525-L527 # noqa: E501
|
247
|
+
@pytest.fixture
|
248
|
+
def unused_tcp_port() -> int:
|
249
|
+
return _unused_port(socket.SOCK_STREAM)
|
250
|
+
|
251
|
+
|
252
|
+
@pytest.fixture(
|
253
|
+
params=[
|
254
|
+
pytest.param(
|
255
|
+
"uvicorn.protocols.websockets.wsproto_impl:WSProtocol",
|
256
|
+
marks=pytest.mark.skipif(not importlib.util.find_spec("wsproto"), reason="wsproto not installed."),
|
257
|
+
id="wsproto",
|
258
|
+
),
|
259
|
+
pytest.param(
|
260
|
+
"uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol",
|
261
|
+
id="websockets",
|
262
|
+
),
|
263
|
+
]
|
264
|
+
)
|
265
|
+
def ws_protocol_cls(request: pytest.FixtureRequest):
|
266
|
+
return import_from_string(request.param)
|
267
|
+
|
268
|
+
|
269
|
+
@pytest.fixture(
|
270
|
+
params=[
|
271
|
+
pytest.param(
|
272
|
+
"uvicorn.protocols.http.httptools_impl:HttpToolsProtocol",
|
273
|
+
marks=pytest.mark.skipif(
|
274
|
+
not importlib.util.find_spec("httptools"),
|
275
|
+
reason="httptools not installed.",
|
276
|
+
),
|
277
|
+
id="httptools",
|
278
|
+
),
|
279
|
+
pytest.param("uvicorn.protocols.http.h11_impl:H11Protocol", id="h11"),
|
280
|
+
]
|
281
|
+
)
|
282
|
+
def http_protocol_cls(request: pytest.FixtureRequest):
|
283
|
+
return import_from_string(request.param)
|
@@ -0,0 +1,53 @@
|
|
1
|
+
import pytest
|
2
|
+
|
3
|
+
from uvicorn.importer import ImportFromStringError, import_from_string
|
4
|
+
|
5
|
+
|
6
|
+
def test_invalid_format() -> None:
|
7
|
+
with pytest.raises(ImportFromStringError) as exc_info:
|
8
|
+
import_from_string("example:")
|
9
|
+
expected = 'Import string "example:" must be in format "<module>:<attribute>".'
|
10
|
+
assert expected in str(exc_info.value)
|
11
|
+
|
12
|
+
|
13
|
+
def test_invalid_module() -> None:
|
14
|
+
with pytest.raises(ImportFromStringError) as exc_info:
|
15
|
+
import_from_string("module_does_not_exist:myattr")
|
16
|
+
expected = 'Could not import module "module_does_not_exist".'
|
17
|
+
assert expected in str(exc_info.value)
|
18
|
+
|
19
|
+
|
20
|
+
def test_invalid_attr() -> None:
|
21
|
+
with pytest.raises(ImportFromStringError) as exc_info:
|
22
|
+
import_from_string("tempfile:attr_does_not_exist")
|
23
|
+
expected = 'Attribute "attr_does_not_exist" not found in module "tempfile".'
|
24
|
+
assert expected in str(exc_info.value)
|
25
|
+
|
26
|
+
|
27
|
+
def test_internal_import_error() -> None:
|
28
|
+
with pytest.raises(ImportError):
|
29
|
+
import_from_string("tests.importer.raise_import_error:myattr")
|
30
|
+
|
31
|
+
|
32
|
+
def test_valid_import() -> None:
|
33
|
+
instance = import_from_string("tempfile:TemporaryFile")
|
34
|
+
from tempfile import TemporaryFile
|
35
|
+
|
36
|
+
assert instance == TemporaryFile
|
37
|
+
|
38
|
+
|
39
|
+
def test_no_import_needed() -> None:
|
40
|
+
from tempfile import TemporaryFile
|
41
|
+
|
42
|
+
instance = import_from_string(TemporaryFile)
|
43
|
+
assert instance == TemporaryFile
|
44
|
+
|
45
|
+
|
46
|
+
def test_circular_import_error() -> None:
|
47
|
+
with pytest.raises(ImportError) as exc_info:
|
48
|
+
import_from_string("tests.importer.circular_import_a:bar")
|
49
|
+
expected = (
|
50
|
+
"cannot import name 'bar' from partially initialized module "
|
51
|
+
"'tests.importer.circular_import_a' (most likely due to a circular import)"
|
52
|
+
)
|
53
|
+
assert expected in str(exc_info.value)
|
@@ -0,0 +1,211 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import contextlib
|
4
|
+
import logging
|
5
|
+
import socket
|
6
|
+
import sys
|
7
|
+
import typing
|
8
|
+
|
9
|
+
import httpx
|
10
|
+
import pytest
|
11
|
+
import websockets
|
12
|
+
import websockets.client
|
13
|
+
|
14
|
+
from tests.utils import run_server
|
15
|
+
from uvicorn import Config
|
16
|
+
from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope
|
17
|
+
|
18
|
+
if typing.TYPE_CHECKING:
|
19
|
+
import sys
|
20
|
+
|
21
|
+
from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol
|
22
|
+
from uvicorn.protocols.websockets.wsproto_impl import WSProtocol as _WSProtocol
|
23
|
+
|
24
|
+
if sys.version_info >= (3, 10): # pragma: no cover
|
25
|
+
from typing import TypeAlias
|
26
|
+
else: # pragma: no cover
|
27
|
+
from typing_extensions import TypeAlias
|
28
|
+
|
29
|
+
WSProtocol: TypeAlias = "type[WebSocketProtocol | _WSProtocol]"
|
30
|
+
|
31
|
+
pytestmark = pytest.mark.anyio
|
32
|
+
|
33
|
+
|
34
|
+
@contextlib.contextmanager
|
35
|
+
def caplog_for_logger(caplog: pytest.LogCaptureFixture, logger_name: str) -> typing.Iterator[pytest.LogCaptureFixture]:
|
36
|
+
logger = logging.getLogger(logger_name)
|
37
|
+
logger.propagate, old_propagate = False, logger.propagate
|
38
|
+
logger.addHandler(caplog.handler)
|
39
|
+
try:
|
40
|
+
yield caplog
|
41
|
+
finally:
|
42
|
+
logger.removeHandler(caplog.handler)
|
43
|
+
logger.propagate = old_propagate
|
44
|
+
|
45
|
+
|
46
|
+
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
|
47
|
+
assert scope["type"] == "http"
|
48
|
+
await send({"type": "http.response.start", "status": 204, "headers": []})
|
49
|
+
await send({"type": "http.response.body", "body": b"", "more_body": False})
|
50
|
+
|
51
|
+
|
52
|
+
async def test_trace_logging(caplog: pytest.LogCaptureFixture, logging_config, unused_tcp_port: int):
|
53
|
+
config = Config(
|
54
|
+
app=app,
|
55
|
+
log_level="trace",
|
56
|
+
log_config=logging_config,
|
57
|
+
lifespan="auto",
|
58
|
+
port=unused_tcp_port,
|
59
|
+
)
|
60
|
+
with caplog_for_logger(caplog, "uvicorn.asgi"):
|
61
|
+
async with run_server(config):
|
62
|
+
async with httpx.AsyncClient() as client:
|
63
|
+
response = await client.get(f"http://127.0.0.1:{unused_tcp_port}")
|
64
|
+
assert response.status_code == 204
|
65
|
+
messages = [record.message for record in caplog.records if record.name == "uvicorn.asgi"]
|
66
|
+
assert "ASGI [1] Started scope=" in messages.pop(0)
|
67
|
+
assert "ASGI [1] Raised exception" in messages.pop(0)
|
68
|
+
assert "ASGI [2] Started scope=" in messages.pop(0)
|
69
|
+
assert "ASGI [2] Send " in messages.pop(0)
|
70
|
+
assert "ASGI [2] Send " in messages.pop(0)
|
71
|
+
assert "ASGI [2] Completed" in messages.pop(0)
|
72
|
+
|
73
|
+
|
74
|
+
async def test_trace_logging_on_http_protocol(http_protocol_cls, caplog, logging_config, unused_tcp_port: int):
|
75
|
+
config = Config(
|
76
|
+
app=app,
|
77
|
+
log_level="trace",
|
78
|
+
http=http_protocol_cls,
|
79
|
+
log_config=logging_config,
|
80
|
+
port=unused_tcp_port,
|
81
|
+
)
|
82
|
+
with caplog_for_logger(caplog, "uvicorn.error"):
|
83
|
+
async with run_server(config):
|
84
|
+
async with httpx.AsyncClient() as client:
|
85
|
+
response = await client.get(f"http://127.0.0.1:{unused_tcp_port}")
|
86
|
+
assert response.status_code == 204
|
87
|
+
messages = [record.message for record in caplog.records if record.name == "uvicorn.error"]
|
88
|
+
assert any(" - HTTP connection made" in message for message in messages)
|
89
|
+
assert any(" - HTTP connection lost" in message for message in messages)
|
90
|
+
|
91
|
+
|
92
|
+
async def test_trace_logging_on_ws_protocol(
|
93
|
+
ws_protocol_cls: WSProtocol,
|
94
|
+
caplog,
|
95
|
+
logging_config,
|
96
|
+
unused_tcp_port: int,
|
97
|
+
):
|
98
|
+
async def websocket_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
|
99
|
+
assert scope["type"] == "websocket"
|
100
|
+
while True:
|
101
|
+
message = await receive()
|
102
|
+
if message["type"] == "websocket.connect":
|
103
|
+
await send({"type": "websocket.accept"})
|
104
|
+
elif message["type"] == "websocket.disconnect":
|
105
|
+
break
|
106
|
+
|
107
|
+
async def open_connection(url):
|
108
|
+
async with websockets.client.connect(url) as websocket:
|
109
|
+
return websocket.open
|
110
|
+
|
111
|
+
config = Config(
|
112
|
+
app=websocket_app,
|
113
|
+
log_level="trace",
|
114
|
+
log_config=logging_config,
|
115
|
+
ws=ws_protocol_cls,
|
116
|
+
port=unused_tcp_port,
|
117
|
+
)
|
118
|
+
with caplog_for_logger(caplog, "uvicorn.error"):
|
119
|
+
async with run_server(config):
|
120
|
+
is_open = await open_connection(f"ws://127.0.0.1:{unused_tcp_port}")
|
121
|
+
assert is_open
|
122
|
+
messages = [record.message for record in caplog.records if record.name == "uvicorn.error"]
|
123
|
+
assert any(" - Upgrading to WebSocket" in message for message in messages)
|
124
|
+
assert any(" - WebSocket connection made" in message for message in messages)
|
125
|
+
assert any(" - WebSocket connection lost" in message for message in messages)
|
126
|
+
|
127
|
+
|
128
|
+
@pytest.mark.parametrize("use_colors", [(True), (False), (None)])
|
129
|
+
async def test_access_logging(use_colors: bool, caplog: pytest.LogCaptureFixture, logging_config, unused_tcp_port: int):
|
130
|
+
config = Config(app=app, use_colors=use_colors, log_config=logging_config, port=unused_tcp_port)
|
131
|
+
with caplog_for_logger(caplog, "uvicorn.access"):
|
132
|
+
async with run_server(config):
|
133
|
+
async with httpx.AsyncClient() as client:
|
134
|
+
response = await client.get(f"http://127.0.0.1:{unused_tcp_port}")
|
135
|
+
|
136
|
+
assert response.status_code == 204
|
137
|
+
messages = [record.message for record in caplog.records if record.name == "uvicorn.access"]
|
138
|
+
assert '"GET / HTTP/1.1" 204' in messages.pop()
|
139
|
+
|
140
|
+
|
141
|
+
@pytest.mark.parametrize("use_colors", [(True), (False)])
|
142
|
+
async def test_default_logging(
|
143
|
+
use_colors: bool, caplog: pytest.LogCaptureFixture, logging_config, unused_tcp_port: int
|
144
|
+
):
|
145
|
+
config = Config(app=app, use_colors=use_colors, log_config=logging_config, port=unused_tcp_port)
|
146
|
+
with caplog_for_logger(caplog, "uvicorn.access"):
|
147
|
+
async with run_server(config):
|
148
|
+
async with httpx.AsyncClient() as client:
|
149
|
+
response = await client.get(f"http://127.0.0.1:{unused_tcp_port}")
|
150
|
+
assert response.status_code == 204
|
151
|
+
messages = [record.message for record in caplog.records if "uvicorn" in record.name]
|
152
|
+
assert "Started server process" in messages.pop(0)
|
153
|
+
assert "Waiting for application startup" in messages.pop(0)
|
154
|
+
assert "ASGI 'lifespan' protocol appears unsupported" in messages.pop(0)
|
155
|
+
assert "Application startup complete" in messages.pop(0)
|
156
|
+
assert "Uvicorn running on http://127.0.0.1" in messages.pop(0)
|
157
|
+
assert '"GET / HTTP/1.1" 204' in messages.pop(0)
|
158
|
+
assert "Shutting down" in messages.pop(0)
|
159
|
+
|
160
|
+
|
161
|
+
@pytest.mark.skipif(sys.platform == "win32", reason="require unix-like system")
|
162
|
+
async def test_running_log_using_uds(
|
163
|
+
caplog: pytest.LogCaptureFixture, short_socket_name: str, unused_tcp_port: int
|
164
|
+
): # pragma: py-win32
|
165
|
+
config = Config(app=app, uds=short_socket_name, port=unused_tcp_port)
|
166
|
+
with caplog_for_logger(caplog, "uvicorn.access"):
|
167
|
+
async with run_server(config):
|
168
|
+
...
|
169
|
+
|
170
|
+
messages = [record.message for record in caplog.records if "uvicorn" in record.name]
|
171
|
+
assert f"Uvicorn running on unix socket {short_socket_name} (Press CTRL+C to quit)" in messages
|
172
|
+
|
173
|
+
|
174
|
+
@pytest.mark.skipif(sys.platform == "win32", reason="require unix-like system")
|
175
|
+
async def test_running_log_using_fd(caplog: pytest.LogCaptureFixture, unused_tcp_port: int): # pragma: py-win32
|
176
|
+
with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
|
177
|
+
fd = sock.fileno()
|
178
|
+
config = Config(app=app, fd=fd, port=unused_tcp_port)
|
179
|
+
with caplog_for_logger(caplog, "uvicorn.access"):
|
180
|
+
async with run_server(config):
|
181
|
+
...
|
182
|
+
sockname = sock.getsockname()
|
183
|
+
messages = [record.message for record in caplog.records if "uvicorn" in record.name]
|
184
|
+
assert f"Uvicorn running on socket {sockname} (Press CTRL+C to quit)" in messages
|
185
|
+
|
186
|
+
|
187
|
+
async def test_unknown_status_code(caplog: pytest.LogCaptureFixture, unused_tcp_port: int):
|
188
|
+
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
|
189
|
+
assert scope["type"] == "http"
|
190
|
+
await send({"type": "http.response.start", "status": 599, "headers": []})
|
191
|
+
await send({"type": "http.response.body", "body": b"", "more_body": False})
|
192
|
+
|
193
|
+
config = Config(app=app, port=unused_tcp_port)
|
194
|
+
with caplog_for_logger(caplog, "uvicorn.access"):
|
195
|
+
async with run_server(config):
|
196
|
+
async with httpx.AsyncClient() as client:
|
197
|
+
response = await client.get(f"http://127.0.0.1:{unused_tcp_port}")
|
198
|
+
|
199
|
+
assert response.status_code == 599
|
200
|
+
messages = [record.message for record in caplog.records if record.name == "uvicorn.access"]
|
201
|
+
assert '"GET / HTTP/1.1" 599' in messages.pop()
|
202
|
+
|
203
|
+
|
204
|
+
async def test_server_start_with_port_zero(caplog: pytest.LogCaptureFixture):
|
205
|
+
config = Config(app=app, port=0)
|
206
|
+
async with run_server(config) as _server:
|
207
|
+
server = _server.servers[0]
|
208
|
+
sock = server.sockets[0]
|
209
|
+
host, port = sock.getsockname()
|
210
|
+
messages = [record.message for record in caplog.records if "uvicorn" in record.name]
|
211
|
+
assert f"Uvicorn running on http://{host}:{port} (Press CTRL+C to quit)" in messages
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import httpx
|
2
|
+
import pytest
|
3
|
+
|
4
|
+
from tests.middleware.test_logging import caplog_for_logger
|
5
|
+
from uvicorn.logging import TRACE_LOG_LEVEL
|
6
|
+
from uvicorn.middleware.message_logger import MessageLoggerMiddleware
|
7
|
+
|
8
|
+
|
9
|
+
@pytest.mark.anyio
|
10
|
+
async def test_message_logger(caplog):
|
11
|
+
async def app(scope, receive, send):
|
12
|
+
await receive()
|
13
|
+
await send({"type": "http.response.start", "status": 200, "headers": []})
|
14
|
+
await send({"type": "http.response.body", "body": b"", "more_body": False})
|
15
|
+
|
16
|
+
with caplog_for_logger(caplog, "uvicorn.asgi"):
|
17
|
+
caplog.set_level(TRACE_LOG_LEVEL, logger="uvicorn.asgi")
|
18
|
+
caplog.set_level(TRACE_LOG_LEVEL)
|
19
|
+
|
20
|
+
transport = httpx.ASGITransport(MessageLoggerMiddleware(app)) # type: ignore
|
21
|
+
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
|
22
|
+
response = await client.get("/")
|
23
|
+
assert response.status_code == 200
|
24
|
+
messages = [record.msg % record.args for record in caplog.records]
|
25
|
+
assert sum(["ASGI [1] Started" in message for message in messages]) == 1
|
26
|
+
assert sum(["ASGI [1] Send" in message for message in messages]) == 2
|
27
|
+
assert sum(["ASGI [1] Receive" in message for message in messages]) == 1
|
28
|
+
assert sum(["ASGI [1] Completed" in message for message in messages]) == 1
|
29
|
+
assert sum(["ASGI [1] Raised exception" in message for message in messages]) == 0
|
30
|
+
|
31
|
+
|
32
|
+
@pytest.mark.anyio
|
33
|
+
async def test_message_logger_exc(caplog):
|
34
|
+
async def app(scope, receive, send):
|
35
|
+
raise RuntimeError()
|
36
|
+
|
37
|
+
with caplog_for_logger(caplog, "uvicorn.asgi"):
|
38
|
+
caplog.set_level(TRACE_LOG_LEVEL, logger="uvicorn.asgi")
|
39
|
+
caplog.set_level(TRACE_LOG_LEVEL)
|
40
|
+
transport = httpx.ASGITransport(MessageLoggerMiddleware(app)) # type: ignore
|
41
|
+
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
|
42
|
+
with pytest.raises(RuntimeError):
|
43
|
+
await client.get("/")
|
44
|
+
messages = [record.msg % record.args for record in caplog.records]
|
45
|
+
assert sum(["ASGI [1] Started" in message for message in messages]) == 1
|
46
|
+
assert sum(["ASGI [1] Send" in message for message in messages]) == 0
|
47
|
+
assert sum(["ASGI [1] Receive" in message for message in messages]) == 0
|
48
|
+
assert sum(["ASGI [1] Completed" in message for message in messages]) == 0
|
49
|
+
assert sum(["ASGI [1] Raised exception" in message for message in messages]) == 1
|