From 9bb227bd143ec2b5c8928c32b6cedfc5f4f21b6a Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Wed, 22 Jun 2022 15:41:04 +0200 Subject: [PATCH] Remove `asgiref` dependency (#1532) * Remove `asgiref` dependency * Format debug.py * Add missing string annotations --- requirements.txt | 3 ++ setup.py | 1 - tests/middleware/test_wsgi.py | 10 +++-- tests/supervisors/test_multiprocess.py | 11 ++++-- tests/test_config.py | 31 +++++++++------ tests/test_subprocess.py | 8 ++-- uvicorn/config.py | 18 +++++++-- uvicorn/lifespan/on.py | 42 ++++++++++---------- uvicorn/main.py | 6 ++- uvicorn/middleware/asgi2.py | 19 +++++---- uvicorn/middleware/debug.py | 50 ++++++++++++++---------- uvicorn/middleware/message_logger.py | 30 ++++++++------ uvicorn/middleware/proxy_headers.py | 27 +++++++------ uvicorn/middleware/wsgi.py | 42 +++++++++++--------- uvicorn/protocols/http/flow_control.py | 22 ++++++----- uvicorn/protocols/http/h11_impl.py | 42 ++++++++++---------- uvicorn/protocols/http/httptools_impl.py | 40 ++++++++++--------- uvicorn/protocols/utils.py | 9 +++-- 18 files changed, 237 insertions(+), 174 deletions(-) diff --git a/requirements.txt b/requirements.txt index 88e460c723..fb915fa24a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,8 @@ -e .[standard] +# Type annotation +asgiref==3.5.2 + # Explicit optionals wsproto==1.1.0 diff --git a/setup.py b/setup.py index 402cd98277..bd0fd27205 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,6 @@ def get_packages(package): env_marker_below_38 = "python_version < '3.8'" minimal_requirements = [ - "asgiref>=3.4.0", "click>=7.0", "h11>=0.8", "typing-extensions;" + env_marker_below_38, diff --git a/tests/middleware/test_wsgi.py b/tests/middleware/test_wsgi.py index 52fab58435..8d56b9e820 100644 --- a/tests/middleware/test_wsgi.py +++ b/tests/middleware/test_wsgi.py @@ -1,14 +1,16 @@ import io import sys -from typing import AsyncGenerator, List +from typing import TYPE_CHECKING, AsyncGenerator, List import httpx import pytest -from asgiref.typing import HTTPRequestEvent, HTTPScope from uvicorn._types import Environ, StartResponse from uvicorn.middleware.wsgi import WSGIMiddleware, build_environ +if TYPE_CHECKING: + from asgiref.typing import HTTPRequestEvent, HTTPScope + def hello_world(environ: Environ, start_response: StartResponse) -> List[bytes]: status = "200 OK" @@ -114,7 +116,7 @@ async def test_wsgi_exc_info() -> None: def test_build_environ_encoding() -> None: - scope: HTTPScope = { + scope: "HTTPScope" = { "asgi": {"version": "3.0", "spec_version": "2.0"}, "scheme": "http", "raw_path": b"/\xe6\x96\x87", @@ -129,7 +131,7 @@ def test_build_environ_encoding() -> None: "headers": [(b"key", b"value1"), (b"key", b"value2")], "extensions": {}, } - message: HTTPRequestEvent = { + message: "HTTPRequestEvent" = { "type": "http.request", "body": b"", "more_body": False, diff --git a/tests/supervisors/test_multiprocess.py b/tests/supervisors/test_multiprocess.py index db22053c84..8638a15e63 100644 --- a/tests/supervisors/test_multiprocess.py +++ b/tests/supervisors/test_multiprocess.py @@ -1,14 +1,17 @@ import signal import socket -from typing import List, Optional - -from asgiref.typing import ASGIReceiveCallable, ASGISendCallable, Scope +from typing import TYPE_CHECKING, List, Optional from uvicorn import Config from uvicorn.supervisors import Multiprocess +if TYPE_CHECKING: + from asgiref.typing import ASGIReceiveCallable, ASGISendCallable, Scope + -def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: +def app( + scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable" +) -> None: pass # pragma: no cover diff --git a/tests/test_config.py b/tests/test_config.py index a2215aa5a3..fb5b9c9c5b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -7,14 +7,8 @@ from pathlib import Path from unittest.mock import MagicMock -if sys.version_info < (3, 8): # pragma: py-gte-38 - from typing_extensions import Literal -else: # pragma: py-lt-38 - from typing import Literal - import pytest import yaml -from asgiref.typing import ASGIApplication, ASGIReceiveCallable, ASGISendCallable, Scope from pytest_mock import MockerFixture from tests.utils import as_cwd @@ -25,6 +19,19 @@ from uvicorn.middleware.wsgi import WSGIMiddleware from uvicorn.protocols.http.h11_impl import H11Protocol +if sys.version_info < (3, 8): # pragma: py-gte-38 + from typing_extensions import Literal +else: # pragma: py-lt-38 + from typing import Literal + +if typing.TYPE_CHECKING: + from asgiref.typing import ( + ASGIApplication, + ASGIReceiveCallable, + ASGISendCallable, + Scope, + ) + @pytest.fixture def mocked_logging_config_module(mocker: MockerFixture) -> MagicMock: @@ -42,7 +49,7 @@ def yaml_logging_config(logging_config: dict) -> str: async def asgi_app( - scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable + scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable" ) -> None: pass # pragma: nocover @@ -64,7 +71,7 @@ def test_debug_app() -> None: [(asgi_app, False), ("tests.test_config:asgi_app", True)], ) def test_config_should_reload_is_set( - app: ASGIApplication, expected_should_reload: bool + app: "ASGIApplication", expected_should_reload: bool ) -> None: config_debug = Config(app=app, debug=True) assert config_debug.debug is True @@ -269,7 +276,7 @@ def test_app_unimportable_other(caplog: pytest.LogCaptureFixture) -> None: def test_app_factory(caplog: pytest.LogCaptureFixture) -> None: - def create_app() -> ASGIApplication: + def create_app() -> "ASGIApplication": return asgi_app config = Config(app=create_app, factory=True, proxy_headers=False) @@ -330,9 +337,9 @@ def test_ssl_config_combined(tls_certificate_key_and_chain_path: str) -> None: assert config.is_ssl is True -def asgi2_app(scope: Scope) -> typing.Callable: +def asgi2_app(scope: "Scope") -> typing.Callable: async def asgi( - receive: ASGIReceiveCallable, send: ASGISendCallable + receive: "ASGIReceiveCallable", send: "ASGISendCallable" ) -> None: # pragma: nocover pass @@ -343,7 +350,7 @@ async def asgi( "app, expected_interface", [(asgi_app, "3.0"), (asgi2_app, "2.0")] ) def test_asgi_version( - app: ASGIApplication, expected_interface: Literal["2.0", "3.0"] + app: "ASGIApplication", expected_interface: Literal["2.0", "3.0"] ) -> None: config = Config(app=app) config.load() diff --git a/tests/test_subprocess.py b/tests/test_subprocess.py index 330a98b2e3..7d46c9f837 100644 --- a/tests/test_subprocess.py +++ b/tests/test_subprocess.py @@ -1,21 +1,23 @@ import socket import sys -from typing import List +from typing import TYPE_CHECKING, List from unittest.mock import patch import pytest -from asgiref.typing import ASGIReceiveCallable, ASGISendCallable, Scope from uvicorn._subprocess import SpawnProcess, get_subprocess, subprocess_started from uvicorn.config import Config +if TYPE_CHECKING: + from asgiref.typing import ASGIReceiveCallable, ASGISendCallable, Scope + def server_run(sockets: List[socket.socket]): # pragma: no cover ... async def app( - scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable + scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable" ) -> None: # pragma: no cover ... diff --git a/uvicorn/config.py b/uvicorn/config.py index 3e772ccab7..5e13fe9fbb 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -8,7 +8,17 @@ import ssl import sys from pathlib import Path -from typing import Awaitable, Callable, Dict, List, Optional, Tuple, Type, Union +from typing import ( + TYPE_CHECKING, + Awaitable, + Callable, + Dict, + List, + Optional, + Tuple, + Type, + Union, +) from h11._connection import DEFAULT_MAX_INCOMPLETE_EVENT_SIZE @@ -20,7 +30,6 @@ from typing import Literal import click -from asgiref.typing import ASGIApplication try: import yaml @@ -37,6 +46,9 @@ from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware from uvicorn.middleware.wsgi import WSGIMiddleware +if TYPE_CHECKING: + from asgiref.typing import ASGIApplication + HTTPProtocolType = Literal["auto", "h11", "httptools"] WSProtocolType = Literal["auto", "none", "websockets", "wsproto"] LifespanType = Literal["auto", "on", "off"] @@ -196,7 +208,7 @@ def _normalize_dirs(dirs: Union[List[str], str, None]) -> List[str]: class Config: def __init__( self, - app: Union[ASGIApplication, Callable, str], + app: Union["ASGIApplication", Callable, str], host: str = "127.0.0.1", port: int = 8000, uds: Optional[str] = None, diff --git a/uvicorn/lifespan/on.py b/uvicorn/lifespan/on.py index caf6e505ec..0c650aab14 100644 --- a/uvicorn/lifespan/on.py +++ b/uvicorn/lifespan/on.py @@ -1,27 +1,29 @@ import asyncio import logging from asyncio import Queue -from typing import Union - -from asgiref.typing import ( - LifespanScope, - LifespanShutdownCompleteEvent, - LifespanShutdownEvent, - LifespanShutdownFailedEvent, - LifespanStartupCompleteEvent, - LifespanStartupEvent, - LifespanStartupFailedEvent, -) +from typing import TYPE_CHECKING, Union from uvicorn import Config -LifespanReceiveMessage = Union[LifespanStartupEvent, LifespanShutdownEvent] -LifespanSendMessage = Union[ - LifespanStartupFailedEvent, - LifespanShutdownFailedEvent, - LifespanStartupCompleteEvent, - LifespanShutdownCompleteEvent, -] +if TYPE_CHECKING: + from asgiref.typing import ( + LifespanScope, + LifespanShutdownCompleteEvent, + LifespanShutdownEvent, + LifespanShutdownFailedEvent, + LifespanStartupCompleteEvent, + LifespanStartupEvent, + LifespanStartupFailedEvent, + ) + + LifespanReceiveMessage = Union[LifespanStartupEvent, LifespanShutdownEvent] + LifespanSendMessage = Union[ + LifespanStartupFailedEvent, + LifespanShutdownFailedEvent, + LifespanStartupCompleteEvent, + LifespanShutdownCompleteEvent, + ] + STATE_TRANSITION_ERROR = "Got invalid state transition on lifespan protocol." @@ -97,7 +99,7 @@ async def main(self) -> None: self.startup_event.set() self.shutdown_event.set() - async def send(self, message: LifespanSendMessage) -> None: + async def send(self, message: "LifespanSendMessage") -> None: assert message["type"] in ( "lifespan.startup.complete", "lifespan.startup.failed", @@ -131,5 +133,5 @@ async def send(self, message: LifespanSendMessage) -> None: if message.get("message"): self.logger.error(message["message"]) - async def receive(self) -> LifespanReceiveMessage: + async def receive(self) -> "LifespanReceiveMessage": return await self.receive_queue.get() diff --git a/uvicorn/main.py b/uvicorn/main.py index 8fccc6b9d0..ac60967220 100644 --- a/uvicorn/main.py +++ b/uvicorn/main.py @@ -6,7 +6,6 @@ import typing import click -from asgiref.typing import ASGIApplication from h11._connection import DEFAULT_MAX_INCOMPLETE_EVENT_SIZE import uvicorn @@ -29,6 +28,9 @@ from uvicorn.server import Server, ServerState # noqa: F401 # Used to be defined here. from uvicorn.supervisors import ChangeReload, Multiprocess +if typing.TYPE_CHECKING: + from asgiref.typing import ASGIApplication + LEVEL_CHOICES = click.Choice(list(LOG_LEVELS.keys())) HTTP_CHOICES = click.Choice(list(HTTP_PROTOCOLS.keys())) WS_CHOICES = click.Choice(list(WS_PROTOCOLS.keys())) @@ -453,7 +455,7 @@ def main( def run( - app: typing.Union[ASGIApplication, str], + app: typing.Union["ASGIApplication", str], *, host: str = "127.0.0.1", port: int = 8000, diff --git a/uvicorn/middleware/asgi2.py b/uvicorn/middleware/asgi2.py index 7ba78e6446..c92b6c8fc5 100644 --- a/uvicorn/middleware/asgi2.py +++ b/uvicorn/middleware/asgi2.py @@ -1,17 +1,20 @@ -from asgiref.typing import ( - ASGI2Application, - ASGIReceiveCallable, - ASGISendCallable, - Scope, -) +import typing + +if typing.TYPE_CHECKING: + from asgiref.typing import ( + ASGI2Application, + ASGIReceiveCallable, + ASGISendCallable, + Scope, + ) class ASGI2Middleware: - def __init__(self, app: ASGI2Application): + def __init__(self, app: "ASGI2Application"): self.app = app async def __call__( - self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable + self, scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable" ) -> None: instance = self.app(scope) await instance(receive, send) diff --git a/uvicorn/middleware/debug.py b/uvicorn/middleware/debug.py index 2ea06ca615..f94b5f670d 100644 --- a/uvicorn/middleware/debug.py +++ b/uvicorn/middleware/debug.py @@ -1,16 +1,17 @@ import html import traceback -from typing import Union +from typing import TYPE_CHECKING, Union -from asgiref.typing import ( - ASGI3Application, - ASGIReceiveCallable, - ASGISendCallable, - ASGISendEvent, - HTTPResponseBodyEvent, - HTTPResponseStartEvent, - WWWScope, -) +if TYPE_CHECKING: + from asgiref.typing import ( + ASGI3Application, + ASGIReceiveCallable, + ASGISendCallable, + ASGISendEvent, + HTTPResponseBodyEvent, + HTTPResponseStartEvent, + WWWScope, + ) class HTMLResponse: @@ -19,16 +20,19 @@ def __init__(self, content: str, status_code: int): self.status_code = status_code async def __call__( - self, scope: WWWScope, receive: ASGIReceiveCallable, send: ASGISendCallable + self, + scope: "WWWScope", + receive: "ASGIReceiveCallable", + send: "ASGISendCallable", ) -> None: - response_start: HTTPResponseStartEvent = { + response_start: "HTTPResponseStartEvent" = { "type": "http.response.start", "status": self.status_code, "headers": [(b"content-type", b"text/html; charset=utf-8")], } await send(response_start) - response_body: HTTPResponseBodyEvent = { + response_body: "HTTPResponseBodyEvent" = { "type": "http.response.body", "body": self.content.encode("utf-8"), "more_body": False, @@ -42,16 +46,19 @@ def __init__(self, content: str, status_code: int): self.status_code = status_code async def __call__( - self, scope: WWWScope, receive: ASGIReceiveCallable, send: ASGISendCallable + self, + scope: "WWWScope", + receive: "ASGIReceiveCallable", + send: "ASGISendCallable", ) -> None: - response_start: HTTPResponseStartEvent = { + response_start: "HTTPResponseStartEvent" = { "type": "http.response.start", "status": self.status_code, "headers": [(b"content-type", b"text/plain; charset=utf-8")], } await send(response_start) - response_body: HTTPResponseBodyEvent = { + response_body: "HTTPResponseBodyEvent" = { "type": "http.response.body", "body": self.content.encode("utf-8"), "more_body": False, @@ -59,7 +66,7 @@ async def __call__( await send(response_body) -def get_accept_header(scope: WWWScope) -> str: +def get_accept_header(scope: "WWWScope") -> str: accept = "*/*" for key, value in scope.get("headers", []): @@ -71,18 +78,21 @@ def get_accept_header(scope: WWWScope) -> str: class DebugMiddleware: - def __init__(self, app: ASGI3Application): + def __init__(self, app: "ASGI3Application"): self.app = app async def __call__( - self, scope: WWWScope, receive: ASGIReceiveCallable, send: ASGISendCallable + self, + scope: "WWWScope", + receive: "ASGIReceiveCallable", + send: "ASGISendCallable", ) -> None: if scope["type"] != "http": return await self.app(scope, receive, send) response_started = False - async def inner_send(message: ASGISendEvent) -> None: + async def inner_send(message: "ASGISendEvent") -> None: nonlocal response_started, send if message["type"] == "http.response.start": diff --git a/uvicorn/middleware/message_logger.py b/uvicorn/middleware/message_logger.py index 43e85f32fd..4738e07528 100644 --- a/uvicorn/middleware/message_logger.py +++ b/uvicorn/middleware/message_logger.py @@ -1,14 +1,15 @@ import logging -from typing import Any +from typing import TYPE_CHECKING, Any -from asgiref.typing import ( - ASGI3Application, - ASGIReceiveCallable, - ASGIReceiveEvent, - ASGISendCallable, - ASGISendEvent, - WWWScope, -) +if TYPE_CHECKING: + from asgiref.typing import ( + ASGI3Application, + ASGIReceiveCallable, + ASGIReceiveEvent, + ASGISendCallable, + ASGISendEvent, + WWWScope, + ) from uvicorn._logging import TRACE_LOG_LEVEL @@ -35,7 +36,7 @@ def message_with_placeholders(message: Any) -> Any: class MessageLoggerMiddleware: - def __init__(self, app: ASGI3Application): + def __init__(self, app: "ASGI3Application"): self.task_counter = 0 self.app = app self.logger = logging.getLogger("uvicorn.asgi") @@ -46,7 +47,10 @@ def trace(message: Any, *args: Any, **kwargs: Any) -> None: self.logger.trace = trace # type: ignore async def __call__( - self, scope: WWWScope, receive: ASGIReceiveCallable, send: ASGISendCallable + self, + scope: "WWWScope", + receive: "ASGIReceiveCallable", + send: "ASGISendCallable", ) -> None: self.task_counter += 1 @@ -54,7 +58,7 @@ async def __call__( client = scope.get("client") prefix = "%s:%d - ASGI" % (client[0], client[1]) if client else "ASGI" - async def inner_receive() -> ASGIReceiveEvent: + async def inner_receive() -> "ASGIReceiveEvent": message = await receive() logged_message = message_with_placeholders(message) log_text = "%s [%d] Receive %s" @@ -63,7 +67,7 @@ async def inner_receive() -> ASGIReceiveEvent: ) return message - async def inner_send(message: ASGISendEvent) -> None: + async def inner_send(message: "ASGISendEvent") -> None: logged_message = message_with_placeholders(message) log_text = "%s [%d] Send %s" self.logger.trace( # type: ignore diff --git a/uvicorn/middleware/proxy_headers.py b/uvicorn/middleware/proxy_headers.py index f81c482374..4b62cf209e 100644 --- a/uvicorn/middleware/proxy_headers.py +++ b/uvicorn/middleware/proxy_headers.py @@ -8,21 +8,24 @@ https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#Proxies """ -from typing import List, Optional, Tuple, Union, cast +from typing import TYPE_CHECKING, List, Optional, Tuple, Union, cast -from asgiref.typing import ( - ASGI3Application, - ASGIReceiveCallable, - ASGISendCallable, - HTTPScope, - Scope, - WebSocketScope, -) +if TYPE_CHECKING: + from asgiref.typing import ( + ASGI3Application, + ASGIReceiveCallable, + ASGISendCallable, + HTTPScope, + Scope, + WebSocketScope, + ) class ProxyHeadersMiddleware: def __init__( - self, app: ASGI3Application, trusted_hosts: Union[List[str], str] = "127.0.0.1" + self, + app: "ASGI3Application", + trusted_hosts: Union[List[str], str] = "127.0.0.1", ) -> None: self.app = app if isinstance(trusted_hosts, str): @@ -44,10 +47,10 @@ def get_trusted_client_host( return None async def __call__( - self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable + self, scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable" ) -> None: if scope["type"] in ("http", "websocket"): - scope = cast(Union[HTTPScope, WebSocketScope], scope) + scope = cast(Union["HTTPScope", "WebSocketScope"], scope) client_addr: Optional[Tuple[str, int]] = scope.get("client") client_host = client_addr[0] if client_addr else None diff --git a/uvicorn/middleware/wsgi.py b/uvicorn/middleware/wsgi.py index f58236acf7..9a79a3baf6 100644 --- a/uvicorn/middleware/wsgi.py +++ b/uvicorn/middleware/wsgi.py @@ -3,24 +3,25 @@ import io import sys from collections import deque -from typing import Deque, Iterable, Optional, Tuple - -from asgiref.typing import ( - ASGIReceiveCallable, - ASGIReceiveEvent, - ASGISendCallable, - ASGISendEvent, - HTTPRequestEvent, - HTTPResponseBodyEvent, - HTTPResponseStartEvent, - HTTPScope, -) +from typing import TYPE_CHECKING, Deque, Iterable, Optional, Tuple + +if TYPE_CHECKING: + from asgiref.typing import ( + ASGIReceiveCallable, + ASGIReceiveEvent, + ASGISendCallable, + ASGISendEvent, + HTTPRequestEvent, + HTTPResponseBodyEvent, + HTTPResponseStartEvent, + HTTPScope, + ) from uvicorn._types import Environ, ExcInfo, StartResponse, WSGIApp def build_environ( - scope: HTTPScope, message: ASGIReceiveEvent, body: io.BytesIO + scope: "HTTPScope", message: "ASGIReceiveEvent", body: io.BytesIO ) -> Environ: """ Builds a scope and request message into a WSGI environ object. @@ -78,7 +79,10 @@ def __init__(self, app: WSGIApp, workers: int = 10): self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=workers) async def __call__( - self, scope: HTTPScope, receive: ASGIReceiveCallable, send: ASGISendCallable + self, + scope: "HTTPScope", + receive: "ASGIReceiveCallable", + send: "ASGISendCallable", ) -> None: assert scope["type"] == "http" instance = WSGIResponder(self.app, self.executor, scope) @@ -90,7 +94,7 @@ def __init__( self, app: WSGIApp, executor: concurrent.futures.ThreadPoolExecutor, - scope: HTTPScope, + scope: "HTTPScope", ): self.app = app self.executor = executor @@ -98,13 +102,13 @@ def __init__( self.status = None self.response_headers = None self.send_event = asyncio.Event() - self.send_queue: Deque[Optional[ASGISendEvent]] = deque() + self.send_queue: Deque[Optional["ASGISendEvent"]] = deque() self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() self.response_started = False self.exc_info: Optional[ExcInfo] = None async def __call__( - self, receive: ASGIReceiveCallable, send: ASGISendCallable + self, receive: "ASGIReceiveCallable", send: "ASGISendCallable" ) -> None: message: HTTPRequestEvent = await receive() # type: ignore[assignment] body = io.BytesIO(message.get("body", b"")) @@ -112,7 +116,7 @@ async def __call__( if more_body: body.seek(0, io.SEEK_END) while more_body: - body_message: HTTPRequestEvent = ( + body_message: "HTTPRequestEvent" = ( await receive() # type: ignore[assignment] ) body.write(body_message.get("body", b"")) @@ -133,7 +137,7 @@ async def __call__( if self.exc_info is not None: raise self.exc_info[0].with_traceback(self.exc_info[1], self.exc_info[2]) - async def sender(self, send: ASGISendCallable) -> None: + async def sender(self, send: "ASGISendCallable") -> None: while True: if self.send_queue: message = self.send_queue.popleft() diff --git a/uvicorn/protocols/http/flow_control.py b/uvicorn/protocols/http/flow_control.py index a499923f34..452c55f430 100644 --- a/uvicorn/protocols/http/flow_control.py +++ b/uvicorn/protocols/http/flow_control.py @@ -1,12 +1,14 @@ import asyncio +import typing -from asgiref.typing import ( - ASGIReceiveCallable, - ASGISendCallable, - HTTPResponseBodyEvent, - HTTPResponseStartEvent, - Scope, -) +if typing.TYPE_CHECKING: + from asgiref.typing import ( + ASGIReceiveCallable, + ASGISendCallable, + HTTPResponseBodyEvent, + HTTPResponseStartEvent, + Scope, + ) CLOSE_HEADER = (b"connection", b"close") @@ -46,9 +48,9 @@ def resume_writing(self) -> None: async def service_unavailable( - scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable + scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable" ) -> None: - response_start: HTTPResponseStartEvent = { + response_start: "HTTPResponseStartEvent" = { "type": "http.response.start", "status": 503, "headers": [ @@ -58,7 +60,7 @@ async def service_unavailable( } await send(response_start) - response_body: HTTPResponseBodyEvent = { + response_body: "HTTPResponseBodyEvent" = { "type": "http.response.body", "body": b"Service Unavailable", "more_body": False, diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py index a21d878a21..e9378c9f0d 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -2,20 +2,10 @@ import http import logging import sys -from typing import Callable, List, Optional, Tuple, Union, cast +from typing import TYPE_CHECKING, Callable, List, Optional, Tuple, Union, cast from urllib.parse import unquote import h11 -from asgiref.typing import ( - ASGI3Application, - ASGIReceiveEvent, - ASGISendEvent, - HTTPDisconnectEvent, - HTTPRequestEvent, - HTTPResponseBodyEvent, - HTTPResponseStartEvent, - HTTPScope, -) from uvicorn._logging import TRACE_LOG_LEVEL from uvicorn.config import Config @@ -39,6 +29,18 @@ else: # pragma: py-lt-38 from typing import Literal +if TYPE_CHECKING: + from asgiref.typing import ( + ASGI3Application, + ASGIReceiveEvent, + ASGISendEvent, + HTTPDisconnectEvent, + HTTPRequestEvent, + HTTPResponseBodyEvent, + HTTPResponseStartEvent, + HTTPScope, + ) + H11Event = Union[ h11.Request, h11.InformationalResponse, @@ -360,7 +362,7 @@ def timeout_keep_alive_handler(self) -> None: class RequestResponseCycle: def __init__( self, - scope: HTTPScope, + scope: "HTTPScope", conn: h11.Connection, transport: asyncio.Transport, flow: FlowControl, @@ -396,7 +398,7 @@ def __init__( self.response_complete = False # ASGI exception wrapper - async def run_asgi(self, app: ASGI3Application) -> None: + async def run_asgi(self, app: "ASGI3Application") -> None: try: result = await app(self.scope, self.receive, self.send) except BaseException as exc: @@ -423,7 +425,7 @@ async def run_asgi(self, app: ASGI3Application) -> None: self.on_response = lambda: None async def send_500_response(self) -> None: - response_start_event: HTTPResponseStartEvent = { + response_start_event: "HTTPResponseStartEvent" = { "type": "http.response.start", "status": 500, "headers": [ @@ -432,7 +434,7 @@ async def send_500_response(self) -> None: ], } await self.send(response_start_event) - response_body_event: HTTPResponseBodyEvent = { + response_body_event: "HTTPResponseBodyEvent" = { "type": "http.response.body", "body": b"Internal Server Error", "more_body": False, @@ -440,7 +442,7 @@ async def send_500_response(self) -> None: await self.send(response_body_event) # ASGI interface - async def send(self, message: ASGISendEvent) -> None: + async def send(self, message: "ASGISendEvent") -> None: message_type = message["type"] if self.flow.write_paused and not self.disconnected: @@ -454,7 +456,7 @@ async def send(self, message: ASGISendEvent) -> None: if message_type != "http.response.start": msg = "Expected ASGI message 'http.response.start', but got '%s'." raise RuntimeError(msg % message_type) - message = cast(HTTPResponseStartEvent, message) + message = cast("HTTPResponseStartEvent", message) self.response_started = True self.waiting_for_100_continue = False @@ -491,7 +493,7 @@ async def send(self, message: ASGISendEvent) -> None: if message_type != "http.response.body": msg = "Expected ASGI message 'http.response.body', but got '%s'." raise RuntimeError(msg % message_type) - message = cast(HTTPResponseBodyEvent, message) + message = cast("HTTPResponseBodyEvent", message) body = message.get("body", b"") more_body = message.get("more_body", False) @@ -524,7 +526,7 @@ async def send(self, message: ASGISendEvent) -> None: self.transport.close() self.on_response() - async def receive(self) -> ASGIReceiveEvent: + async def receive(self) -> "ASGIReceiveEvent": if self.waiting_for_100_continue and not self.transport.is_closing(): event = h11.InformationalResponse( status_code=100, headers=[], reason="Continue" @@ -538,7 +540,7 @@ async def receive(self) -> ASGIReceiveEvent: await self.message_event.wait() self.message_event.clear() - message: Union[HTTPDisconnectEvent, HTTPRequestEvent] + message: "Union[HTTPDisconnectEvent, HTTPRequestEvent]" if self.disconnected or self.response_complete: message = {"type": "http.disconnect"} else: diff --git a/uvicorn/protocols/http/httptools_impl.py b/uvicorn/protocols/http/httptools_impl.py index 1135ae493e..b71f3b491b 100644 --- a/uvicorn/protocols/http/httptools_impl.py +++ b/uvicorn/protocols/http/httptools_impl.py @@ -6,19 +6,9 @@ import urllib from asyncio.events import TimerHandle from collections import deque -from typing import Callable, Deque, List, Optional, Tuple, Union, cast +from typing import TYPE_CHECKING, Callable, Deque, List, Optional, Tuple, Union, cast import httptools -from asgiref.typing import ( - ASGI3Application, - ASGIReceiveEvent, - ASGISendEvent, - HTTPDisconnectEvent, - HTTPRequestEvent, - HTTPResponseBodyEvent, - HTTPResponseStartEvent, - HTTPScope, -) from uvicorn._logging import TRACE_LOG_LEVEL from uvicorn.config import Config @@ -42,6 +32,18 @@ else: # pragma: py-lt-38 from typing import Literal +if TYPE_CHECKING: + from asgiref.typing import ( + ASGI3Application, + ASGIReceiveEvent, + ASGISendEvent, + HTTPDisconnectEvent, + HTTPRequestEvent, + HTTPResponseBodyEvent, + HTTPResponseStartEvent, + HTTPScope, + ) + HEADER_RE = re.compile(b'[\x00-\x1F\x7F()<>@,;:[]={} \t\\"]') HEADER_VALUE_RE = re.compile(b"[\x00-\x1F\x7F]") @@ -356,7 +358,7 @@ def timeout_keep_alive_handler(self) -> None: class RequestResponseCycle: def __init__( self, - scope: HTTPScope, + scope: "HTTPScope", transport: asyncio.Transport, flow: FlowControl, logger: logging.Logger, @@ -394,7 +396,7 @@ def __init__( self.expected_content_length = 0 # ASGI exception wrapper - async def run_asgi(self, app: ASGI3Application) -> None: + async def run_asgi(self, app: "ASGI3Application") -> None: try: result = await app(self.scope, self.receive, self.send) except BaseException as exc: @@ -421,7 +423,7 @@ async def run_asgi(self, app: ASGI3Application) -> None: self.on_response = lambda: None async def send_500_response(self) -> None: - response_start_event: HTTPResponseStartEvent = { + response_start_event: "HTTPResponseStartEvent" = { "type": "http.response.start", "status": 500, "headers": [ @@ -430,7 +432,7 @@ async def send_500_response(self) -> None: ], } await self.send(response_start_event) - response_body_event: HTTPResponseBodyEvent = { + response_body_event: "HTTPResponseBodyEvent" = { "type": "http.response.body", "body": b"Internal Server Error", "more_body": False, @@ -438,7 +440,7 @@ async def send_500_response(self) -> None: await self.send(response_body_event) # ASGI interface - async def send(self, message: ASGISendEvent) -> None: + async def send(self, message: "ASGISendEvent") -> None: message_type = message["type"] if self.flow.write_paused and not self.disconnected: @@ -452,7 +454,7 @@ async def send(self, message: ASGISendEvent) -> None: if message_type != "http.response.start": msg = "Expected ASGI message 'http.response.start', but got '%s'." raise RuntimeError(msg % message_type) - message = cast(HTTPResponseStartEvent, message) + message = cast("HTTPResponseStartEvent", message) self.response_started = True self.waiting_for_100_continue = False @@ -548,7 +550,7 @@ async def send(self, message: ASGISendEvent) -> None: msg = "Unexpected ASGI message '%s' sent, after response already completed." raise RuntimeError(msg % message_type) - async def receive(self) -> ASGIReceiveEvent: + async def receive(self) -> "ASGIReceiveEvent": if self.waiting_for_100_continue and not self.transport.is_closing(): self.transport.write(b"HTTP/1.1 100 Continue\r\n\r\n") self.waiting_for_100_continue = False @@ -558,7 +560,7 @@ async def receive(self) -> ASGIReceiveEvent: await self.message_event.wait() self.message_event.clear() - message: Union[HTTPDisconnectEvent, HTTPRequestEvent] + message: "Union[HTTPDisconnectEvent, HTTPRequestEvent]" if self.disconnected or self.response_complete: message = {"type": "http.disconnect"} else: diff --git a/uvicorn/protocols/utils.py b/uvicorn/protocols/utils.py index ce71dbb610..fbd4b4d5db 100644 --- a/uvicorn/protocols/utils.py +++ b/uvicorn/protocols/utils.py @@ -1,8 +1,9 @@ import asyncio import urllib.parse -from typing import Optional, Tuple +from typing import TYPE_CHECKING, Optional, Tuple -from asgiref.typing import WWWScope +if TYPE_CHECKING: + from asgiref.typing import WWWScope def get_remote_addr(transport: asyncio.Transport) -> Optional[Tuple[str, int]]: @@ -38,14 +39,14 @@ def is_ssl(transport: asyncio.Transport) -> bool: return bool(transport.get_extra_info("sslcontext")) -def get_client_addr(scope: WWWScope) -> str: +def get_client_addr(scope: "WWWScope") -> str: client = scope.get("client") if not client: return "" return "%s:%d" % client -def get_path_with_query_string(scope: WWWScope) -> str: +def get_path_with_query_string(scope: "WWWScope") -> str: path_with_query_string = urllib.parse.quote(scope["path"]) if scope["query_string"]: path_with_query_string = "{}?{}".format(