Skip to content

Alternative lifespan state api - #2065

Merged
adriangb merged 4 commits into
Kludex:masterfrom
adriangb:alternative-lifespan-state-api
Mar 9, 2023
Merged

adriangb merged 4 commits into
Kludex:masterfrom
adriangb:alternative-lifespan-state-api

Conversation

@adriangb

@adriangb adriangb commented Mar 5, 2023

Copy link
Copy Markdown
Contributor

The first commit reverts the current unreleased API, the second adds the proposed API. I can do the same thing for the docs.

I think one of the interesting things this opens up is that by having a concrete type for the user's state we could use https://peps.python.org/pep-0696/ in the future to do:

from typing_extensions import TypeVar  # support any Python version
from starlette.datastructures import State

StateType = TypeVar("StateType", default=State)

class Request(Generic[StateType]):
    ...

So users can fully type their use of state with minimal boilerplate:

class State(TypedDict):
    foo: int

@asynccontextmanager
async def lifespan(app) -> State:
    yield State(foo=1)

async def endpoint(request: Request[State]) -> Response:
    request.state["foo"] + 1  # type checks correctly

Or if they are adding more keys to the request:

class LifespanState(TypedDict):
    foo: int

class State(LifespanState):
   user: str | None

@asynccontextmanager
async def lifespan(app) -> LifespanState:
    yield State(foo=1)

async def endpoint(request: Request[State]) -> Response:
    request.state["foo"] + 1  # type checks correctly

Or something like that.

@adriangb
adriangb requested a review from Kludex March 5, 2023 20:57
@adriangb
adriangb force-pushed the alternative-lifespan-state-api branch from ec2eafa to 4e5ec27 Compare March 5, 2023 21:06
@Kludex

Kludex commented Mar 5, 2023

Copy link
Copy Markdown
Owner

What you are proposing is to not add the state to startup and shutdown at all?

What you mention about the PEP-696 can still be done without this PR getting in.

@adriangb
adriangb force-pushed the alternative-lifespan-state-api branch from 4e5ec27 to 5e36c4c Compare March 5, 2023 21:09
@adriangb

adriangb commented Mar 5, 2023

Copy link
Copy Markdown
Contributor Author

What you are proposing is to not add the state to startup and shutdown at all?

Yes. I think that's pretty reasonable.

What you mention about the PEP-696 can still be done without this PR getting in.

Yes it can but with the current API you can't really use a type in your lifespan which I think makes it a bit less interesting.

(well you can, but you'd be doing something like state.update(MyTypedDict(foo=1)), which is kinda weird)

@adriangb
adriangb force-pushed the alternative-lifespan-state-api branch from 5e36c4c to 14fd988 Compare March 5, 2023 21:12
@Kludex

Kludex commented Mar 5, 2023

Copy link
Copy Markdown
Owner

I'm against not supporting startup and shutdown.

If you'd like, we can revert the two previous commits (implementation, and docs), and discuss a bit more about what we want to do.

I'll personally not be in favor of removing or not supporting state on startup and shutdown, but maybe we can just change the signature from () -> None to (state) -> None instead of introspecting on 1.0?

@adriangb

adriangb commented Mar 5, 2023

Copy link
Copy Markdown
Contributor Author

I'm against not supporting startup and shutdown.

We could always add that later, with the current API...

@Kludex Kludex left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only documentation is missing here, and we can go with it. 馃憤

Comment thread starlette/types.py
Comment on lines +14 to +17
StatelessLifespan = typing.Callable[["Starlette"], typing.AsyncContextManager[None]]
StatefulLifespan = typing.Callable[
["Starlette"], typing.AsyncContextManager[typing.Mapping[str, typing.Any]]
]

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be ASGIApp instead of Starlette?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well we always pass in a Starlette object right? What good does it do to receive a reference to a generic ASGIApp object?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about FastAPI when I asked that, when the super() is called on Router, they will have an issue over there, I guess? Also, because this module has ASGI related types.

In any case, not that important.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah probably need a self: SelfType, lifespan: Lifespan[SelfType] or something. Or users can just cast it. Also I don't think there's any FastAPI specific methods they'd want to access from the lifepsan. And with this state thing there's almost no reason to even use the app argument anyway...

@Kludex

Kludex commented Mar 8, 2023

Copy link
Copy Markdown
Owner

After this PR, we can deprecate on_shutdown and on_startup, with something like:

The on_startup and on_shutdown parameters are deprecated, and they
will be removed on version 1.0. Use the lifespan parameter instead.
See more about it on https://www.starlette.io/events/.

And maybe changing the name events.md by lifespan.md... 馃槄

I can do this follow up, I just need this to be merged. 馃憤

@Kludex

Kludex commented Mar 9, 2023

Copy link
Copy Markdown
Owner

I'm going to do a follow-up with the deprecation of on_startup and on_shutdown.

@adriangb Please check #2065 (comment), and my last commit to see if you are fine with it.

@adriangb
adriangb merged commit 92ab71e into Kludex:master Mar 9, 2023
@adriangb
adriangb deleted the alternative-lifespan-state-api branch March 9, 2023 20:04
Comment thread starlette/testclient.py

async def lifespan(self) -> None:
scope = {"type": "lifespan"}
scope = {"type": "lifespan", "state": self.app_state}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@florimondmanca like this

@FaydSpeare

FaydSpeare commented Jul 20, 2023

Copy link
Copy Markdown

It would be nice to give a way for users to have type hints on their request.state. There's an example in the Lifespan section of the starlette docs that uses a TypedDict, but inside your routes that doesn't offer you any type hints. It also doesn't make a lot of sense, since you're accessing attributes on state (state.count), which is not how you access fields of a dictionary (or TypedDict).

I was just trying to give myself type hints by creating a subclass of Request, MyRequest, and using the MyState TypedDict as the type for its state attribute. This already required # type: ignore[assignment]. But when you go to use MyRequest in your routes, type hints will now tell you that req.state.count doesn't exists, but req.state["count"] does, which isn't the case.
Is TypedDict not essentially an incorrect type for starlette State?

You could use a dataclass instead of a TypedDict, but the typing for the lifespan won't accept that.

So it would be quite nice to have something like Request[MyState] which allows you to do this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants