diff --git a/CHANGES.md b/CHANGES.md index dc4fed0..4c10c09 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,7 @@ Released 2026-07-23 - Support partitioned cookies - Don't apply max_form_parts to non-multipart forms +- Allow assigning ASGI middleware to ``asgi_app`` without type errors. ## Version 0.20.0 diff --git a/src/quart/app.py b/src/quart/app.py index f5ae35f..6fe8e4f 100644 --- a/src/quart/app.py +++ b/src/quart/app.py @@ -20,6 +20,7 @@ from typing import NoReturn from typing import overload from typing import ParamSpec +from typing import TYPE_CHECKING from typing import TypeVar from urllib.parse import quote @@ -94,6 +95,7 @@ from .testing import TestApp from .typing import AfterServingCallable from .typing import AfterWebsocketCallable +from .typing import ASGIApp from .typing import ASGIHTTPProtocol from .typing import ASGILifespanProtocol from .typing import ASGIWebsocketProtocol @@ -1735,31 +1737,37 @@ async def __call__( """ await self.asgi_app(scope, receive, send) - async def asgi_app( - self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable - ) -> None: - """This handles ASGI calls, it can be wrapped in middleware. + if TYPE_CHECKING: + asgi_app: ASGIApp + else: - When using middleware with Quart it is preferable to wrap this - method rather than the app itself. This is to ensure that the - app is an instance of this class - which allows the quart cli - to work correctly. To use this feature simply do, + async def asgi_app( + self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable + ) -> None: + """This handles ASGI calls, it can be wrapped in middleware. - .. code-block:: python + When using middleware with Quart it is preferable to wrap this + method rather than the app itself. This is to ensure that the + app is an instance of this class - which allows the quart cli + to work correctly. To use this feature simply do, - app.asgi_app = middleware(app.asgi_app) + .. code-block:: python - """ - asgi_handler: ASGIHTTPProtocol | ASGILifespanProtocol | ASGIWebsocketProtocol - if scope["type"] == "http": - asgi_handler = self.asgi_http_class(self, scope) - elif scope["type"] == "websocket": - asgi_handler = self.asgi_websocket_class(self, scope) - elif scope["type"] == "lifespan": - asgi_handler = self.asgi_lifespan_class(self, scope) - else: - raise RuntimeError("ASGI Scope type is unknown") - await asgi_handler(receive, send) + app.asgi_app = middleware(app.asgi_app) + + """ + asgi_handler: ( + ASGIHTTPProtocol | ASGILifespanProtocol | ASGIWebsocketProtocol + ) + if scope["type"] == "http": + asgi_handler = self.asgi_http_class(self, scope) + elif scope["type"] == "websocket": + asgi_handler = self.asgi_websocket_class(self, scope) + elif scope["type"] == "lifespan": + asgi_handler = self.asgi_lifespan_class(self, scope) + else: + raise RuntimeError("ASGI Scope type is unknown") + await asgi_handler(receive, send) async def startup(self) -> None: self.shutdown_event = self.event_class() diff --git a/src/quart/typing.py b/src/quart/typing.py index 0681f62..dbc8631 100644 --- a/src/quart/typing.py +++ b/src/quart/typing.py @@ -35,6 +35,7 @@ from .wrappers.response import Response # noqa: F401 FilePath = bytes | str | os.PathLike +ASGIApp = Callable[[Any, Any, Any], Awaitable[None]] # The possible types that are directly convertible or are a Response object. ResponseValue = Union[ diff --git a/tests/test_testing.py b/tests/test_testing.py index 4e878f4..619a6dd 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -458,7 +458,7 @@ async def __call__( else: await self.app(scope, receive, send) - app.asgi_app = OddMiddleware(app.asgi_app) # type: ignore + app.asgi_app = OddMiddleware(app.asgi_app) client = app.test_client() response = await client.get("/")