uvicorn 0.30.6__tar.gz → 0.31.1__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.30.6 → uvicorn-0.31.1}/PKG-INFO +1 -1
  2. {uvicorn-0.30.6 → uvicorn-0.31.1}/pyproject.toml +6 -1
  3. uvicorn-0.31.1/requirements.txt +32 -0
  4. uvicorn-0.31.1/tests/conftest.py +283 -0
  5. uvicorn-0.31.1/tests/importer/circular_import_a.py +4 -0
  6. uvicorn-0.31.1/tests/importer/circular_import_b.py +4 -0
  7. uvicorn-0.31.1/tests/importer/raise_import_error.py +5 -0
  8. uvicorn-0.31.1/tests/importer/test_importer.py +53 -0
  9. uvicorn-0.31.1/tests/middleware/test_logging.py +211 -0
  10. uvicorn-0.31.1/tests/middleware/test_message_logger.py +49 -0
  11. uvicorn-0.31.1/tests/middleware/test_proxy_headers.py +494 -0
  12. uvicorn-0.31.1/tests/middleware/test_wsgi.py +138 -0
  13. uvicorn-0.31.1/tests/protocols/test_http.py +1126 -0
  14. uvicorn-0.31.1/tests/protocols/test_utils.py +85 -0
  15. uvicorn-0.31.1/tests/protocols/test_websocket.py +1190 -0
  16. uvicorn-0.31.1/tests/response.py +37 -0
  17. uvicorn-0.31.1/tests/supervisors/test_multiprocess.py +171 -0
  18. uvicorn-0.31.1/tests/supervisors/test_reload.py +437 -0
  19. uvicorn-0.31.1/tests/supervisors/test_signal.py +104 -0
  20. uvicorn-0.31.1/tests/test_auto_detection.py +59 -0
  21. uvicorn-0.31.1/tests/test_cli.py +203 -0
  22. uvicorn-0.31.1/tests/test_config.py +547 -0
  23. uvicorn-0.31.1/tests/test_default_headers.py +69 -0
  24. uvicorn-0.31.1/tests/test_lifespan.py +264 -0
  25. uvicorn-0.31.1/tests/test_main.py +115 -0
  26. uvicorn-0.31.1/tests/test_server.py +67 -0
  27. uvicorn-0.31.1/tests/test_ssl.py +94 -0
  28. uvicorn-0.31.1/tests/test_subprocess.py +43 -0
  29. uvicorn-0.31.1/tests/utils.py +46 -0
  30. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/__init__.py +1 -1
  31. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/config.py +1 -1
  32. uvicorn-0.31.1/uvicorn/loops/__init__.py +0 -0
  33. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/main.py +5 -3
  34. uvicorn-0.31.1/uvicorn/middleware/__init__.py +0 -0
  35. uvicorn-0.31.1/uvicorn/middleware/proxy_headers.py +142 -0
  36. uvicorn-0.31.1/uvicorn/protocols/__init__.py +0 -0
  37. uvicorn-0.31.1/uvicorn/protocols/http/__init__.py +0 -0
  38. uvicorn-0.31.1/uvicorn/protocols/websockets/__init__.py +0 -0
  39. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/protocols/websockets/websockets_impl.py +4 -8
  40. uvicorn-0.30.6/uvicorn/middleware/proxy_headers.py +0 -70
  41. {uvicorn-0.30.6 → uvicorn-0.31.1}/.gitignore +0 -0
  42. {uvicorn-0.30.6 → uvicorn-0.31.1}/LICENSE.md +0 -0
  43. {uvicorn-0.30.6 → uvicorn-0.31.1}/README.md +0 -0
  44. {uvicorn-0.30.6/uvicorn/lifespan → uvicorn-0.31.1/tests}/__init__.py +0 -0
  45. {uvicorn-0.30.6/uvicorn/loops → uvicorn-0.31.1/tests/importer}/__init__.py +0 -0
  46. {uvicorn-0.30.6/uvicorn → uvicorn-0.31.1/tests}/middleware/__init__.py +0 -0
  47. {uvicorn-0.30.6/uvicorn → uvicorn-0.31.1/tests}/protocols/__init__.py +0 -0
  48. {uvicorn-0.30.6/uvicorn/protocols/http → uvicorn-0.31.1/tests/supervisors}/__init__.py +0 -0
  49. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/__main__.py +0 -0
  50. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/_subprocess.py +0 -0
  51. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/_types.py +0 -0
  52. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/importer.py +0 -0
  53. {uvicorn-0.30.6/uvicorn/protocols/websockets → uvicorn-0.31.1/uvicorn/lifespan}/__init__.py +0 -0
  54. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/lifespan/off.py +0 -0
  55. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/lifespan/on.py +0 -0
  56. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/logging.py +0 -0
  57. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/loops/asyncio.py +0 -0
  58. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/loops/auto.py +0 -0
  59. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/loops/uvloop.py +0 -0
  60. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/middleware/asgi2.py +0 -0
  61. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/middleware/message_logger.py +0 -0
  62. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/middleware/wsgi.py +0 -0
  63. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/protocols/http/auto.py +0 -0
  64. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/protocols/http/flow_control.py +0 -0
  65. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/protocols/http/h11_impl.py +0 -0
  66. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/protocols/http/httptools_impl.py +0 -0
  67. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/protocols/utils.py +0 -0
  68. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/protocols/websockets/auto.py +0 -0
  69. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/protocols/websockets/wsproto_impl.py +0 -0
  70. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/py.typed +0 -0
  71. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/server.py +0 -0
  72. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/supervisors/__init__.py +0 -0
  73. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/supervisors/basereload.py +0 -0
  74. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/supervisors/multiprocess.py +0 -0
  75. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/supervisors/statreload.py +0 -0
  76. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/supervisors/watchfilesreload.py +0 -0
  77. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/supervisors/watchgodreload.py +0 -0
  78. {uvicorn-0.30.6 → uvicorn-0.31.1}/uvicorn/workers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: uvicorn
3
- Version: 0.30.6
3
+ Version: 0.31.1
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
@@ -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 = ["/uvicorn"]
62
+ include = [
63
+ "/uvicorn",
64
+ "/tests",
65
+ "/requirements.txt",
66
+ ]
63
67
 
64
68
  [tool.ruff]
65
69
  line-length = 120
@@ -92,6 +96,7 @@ filterwarnings = [
92
96
  'ignore: \"watchgod\" is deprecated\, you should switch to watchfiles \(`pip install watchfiles`\)\.:DeprecationWarning',
93
97
  "ignore:Uvicorn's native WSGI implementation is deprecated.*:DeprecationWarning",
94
98
  "ignore: 'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning",
99
+ "ignore: remove second argument of ws_handler:DeprecationWarning:websockets"
95
100
  ]
96
101
 
97
102
  [tool.coverage.run]
@@ -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.7
9
+ wsproto==1.2.0
10
+ websockets==13.1
11
+
12
+ # Packaging
13
+ build==1.2.2
14
+ twine==5.1.1
15
+
16
+ # Testing
17
+ ruff==0.6.8
18
+ pytest==8.3.3
19
+ pytest-mock==3.14.0
20
+ mypy==1.11.2
21
+ types-click==7.1.8
22
+ types-pyyaml==6.0.12.20240917
23
+ trustme==1.1.0
24
+ cryptography==43.0.1
25
+ coverage==7.6.1
26
+ coverage-conditional-plugin==0.9.0
27
+ httpx==0.27.2
28
+ watchgod==0.8.2
29
+
30
+ # Documentation
31
+ mkdocs==1.6.1
32
+ mkdocs-material==9.5.39
@@ -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,4 @@
1
+ # Used by test_importer.py
2
+ from .circular_import_b import foo # noqa
3
+
4
+ bar = 123 # pragma: no cover
@@ -0,0 +1,4 @@
1
+ # Used by test_importer.py
2
+ from .circular_import_a import bar # noqa
3
+
4
+ foo = 123 # pragma: no cover
@@ -0,0 +1,5 @@
1
+ # Used by test_importer.py
2
+
3
+ myattr = 123
4
+
5
+ import does_not_exist # noqa
@@ -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