Skip to content

Commit

Permalink
Fix typing of Lifespan to allow subclasses of Starlette (#2077)
Browse files Browse the repository at this point in the history
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
  • Loading branch information
adriangb and Kludex committed Mar 13, 2023
1 parent ada845c commit f640241
Show file tree
Hide file tree
Showing 4 changed files with 26 additions and 9 deletions.
6 changes: 4 additions & 2 deletions starlette/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from starlette.routing import BaseRoute, Router
from starlette.types import ASGIApp, Lifespan, Receive, Scope, Send

AppType = typing.TypeVar("AppType", bound="Starlette")


class Starlette:
"""
Expand Down Expand Up @@ -43,7 +45,7 @@ class Starlette:
"""

def __init__(
self,
self: "AppType",
debug: bool = False,
routes: typing.Optional[typing.Sequence[BaseRoute]] = None,
middleware: typing.Optional[typing.Sequence[Middleware]] = None,
Expand All @@ -58,7 +60,7 @@ def __init__(
] = None,
on_startup: typing.Optional[typing.Sequence[typing.Callable]] = None,
on_shutdown: typing.Optional[typing.Sequence[typing.Callable]] = None,
lifespan: typing.Optional[Lifespan] = None,
lifespan: typing.Optional[Lifespan["AppType"]] = None,
) -> None:
# The lifespan context function is a newer style that replaces
# on_startup / on_shutdown handlers. Use one or the other, not both.
Expand Down
4 changes: 3 additions & 1 deletion starlette/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,9 @@ def __init__(
default: typing.Optional[ASGIApp] = None,
on_startup: typing.Optional[typing.Sequence[typing.Callable]] = None,
on_shutdown: typing.Optional[typing.Sequence[typing.Callable]] = None,
lifespan: typing.Optional[Lifespan] = None,
# the generic to Lifespan[AppType] is the type of the top level application
# which the router cannot know statically, so we use typing.Any
lifespan: typing.Optional[Lifespan[typing.Any]] = None,
) -> None:
self.routes = [] if routes is None else list(routes)
self.redirect_slashes = redirect_slashes
Expand Down
9 changes: 4 additions & 5 deletions starlette/types.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import typing

if typing.TYPE_CHECKING:
from starlette.applications import Starlette
AppType = typing.TypeVar("AppType")

Scope = typing.MutableMapping[str, typing.Any]
Message = typing.MutableMapping[str, typing.Any]
Expand All @@ -11,8 +10,8 @@

ASGIApp = typing.Callable[[Scope, Receive, Send], typing.Awaitable[None]]

StatelessLifespan = typing.Callable[["Starlette"], typing.AsyncContextManager[None]]
StatelessLifespan = typing.Callable[[AppType], typing.AsyncContextManager[None]]
StatefulLifespan = typing.Callable[
["Starlette"], typing.AsyncContextManager[typing.Mapping[str, typing.Any]]
[AppType], typing.AsyncContextManager[typing.Mapping[str, typing.Any]]
]
Lifespan = typing.Union[StatelessLifespan, StatefulLifespan]
Lifespan = typing.Union[StatelessLifespan[AppType], StatefulLifespan[AppType]]
16 changes: 15 additions & 1 deletion tests/test_applications.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os
from contextlib import asynccontextmanager
from typing import Any, Callable
from typing import Any, AsyncIterator, Callable

import anyio
import httpx
Expand Down Expand Up @@ -534,3 +534,17 @@ def get_app() -> ASGIApp:
test_client_factory(app).get("/foo")

assert SimpleInitializableMiddleware.counter == 2


def test_lifespan_app_subclass():
# This test exists to make sure that subclasses of Starlette
# (like FastAPI) are compatible with the types hints for Lifespan

class App(Starlette):
pass

@asynccontextmanager
async def lifespan(app: App) -> AsyncIterator[None]: # pragma: no cover
yield

App(lifespan=lifespan)

0 comments on commit f640241

Please sign in to comment.