Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support lifespan state #2060

Merged
merged 7 commits into from
Mar 5, 2023
Merged

Support lifespan state #2060

merged 7 commits into from
Mar 5, 2023

Conversation

Kludex
Copy link
Member

@Kludex Kludex commented Mar 4, 2023

Changes

  • Implement lifespan state.
  • Remove generator function way to use lifespan.

How to review

You need to have both this branch and the one from encode/uvicorn#1818 on your virtual environment, and then you can use this application to test the behavior:

from contextlib import asynccontextmanager
from typing import Any, Dict

from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
from starlette.routing import Route


# async def startup(state: Dict[str, Any]):
#     print(state)
#     state["potato"] = True
#     print(state)
#     print("startup")


# async def stateless_startup():
#     print("stateless_startup")


@asynccontextmanager
async def state_manager(app, state):
    state["haha"] = True
    print(state)
    yield
    print(state)

@asynccontextmanager
async def stateless_manager(app):
    print("stateless manager")
    yield

async def homepage(request):
    print(request.state)
    return PlainTextResponse("Hello, world!")


app = Starlette(
    routes=[Route("/", homepage)],
    # on_startup=[startup, stateless_startup],
    lifespan=state_manager,
    # lifespan=stateless_manager,
)

Play with the comments above. 👀

"""
Run any `.on_startup` event handlers.
"""
for handler in self.on_startup:
sig = inspect.signature(handler)
if len(sig.parameters) == 1 and state is not None:
Copy link
Member Author

Choose a reason for hiding this comment

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

If the state is None, it means that the server doesn't support it, so we'll maintain the same error message.

Copy link
Member

@adriangb adriangb left a comment

Choose a reason for hiding this comment

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

Overall looks good! I have some questions about deprecations, unless it makes the PR a lot more complicated I would suggest doing the deprecations in another PR. You could always support this new feature only for async context manager dependencies to avoid having to implement things for all of the other types of lifespans / startup/shutdown events we support. But not sure how that works out code wise.

Comment on lines +543 to 549
def __call__(
self: _TDefaultLifespan,
app: object,
state: typing.Optional[typing.Dict[str, typing.Any]],
) -> _TDefaultLifespan:
self._state = state
return self
Copy link
Member

Choose a reason for hiding this comment

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

At first glance I'm a bit confused about this and the need for a TypeVar.

Copy link
Member Author

@Kludex Kludex Mar 4, 2023

Choose a reason for hiding this comment

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

The reason for the TypeVar is that it needed to be bound, as the _state was introduced here, since the self is annotated.

starlette/routing.py Outdated Show resolved Hide resolved
@@ -243,6 +245,7 @@ def handle_request(self, request: httpx.Request) -> httpx.Response:
"client": ["testclient", 50000],
"server": [host, port],
"subprotocols": subprotocols,
"state": self.state,
Copy link
Member

Choose a reason for hiding this comment

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

I'm not seeing where this dict is copied. Is it being copied before being passed into each request?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ups 👀

starlette/types.py Outdated Show resolved Hide resolved
Comment on lines 680 to 688
async def run_startup(state):
nonlocal startup_complete
startup_complete = True
state["startup"] = True

async def run_shutdown(state):
nonlocal shutdown_complete
shutdown_complete = True
assert state["startup"]
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to support state in on_startup and on_shutdown? Don't we want to deprecate that in 1.0? If we were not planning on it, we should be. I think we should only support async context manager lifespans in 1.0.

Copy link
Member Author

Choose a reason for hiding this comment

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

Do we want to support state in on_startup and on_shutdown?

I don't think we discussed it.

What are the problems we have there besides "multiple ways to do things"?

Copy link
Member

Choose a reason for hiding this comment

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

I'd say just that: multiple ways to do things.

Copy link
Member Author

Choose a reason for hiding this comment

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

I really like the on_startup and on_shutdown parameters, and given that it would be one more thing to do for 1.0, I'm not in favor of doing it.

Copy link
Member

Choose a reason for hiding this comment

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

Okay fair enough, it's besides the point of this PR. Not sure why you like them but I'm fairly certain the team decided to promote context manager lifespans as the way forward, so can we at least write the tests using that?

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you know where it was discussed?

Copy link
Member

Choose a reason for hiding this comment

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

Not specifically, just my recollection. Maybe @graingert does, I know they advocated heavilty for the async context manager approach.

tests/test_routing.py Outdated Show resolved Hide resolved
tests/test_routing.py Outdated Show resolved Hide resolved
tests/test_routing.py Outdated Show resolved Hide resolved
Kludex and others added 2 commits March 4, 2023 20:00
Co-authored-by: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>
@Kludex Kludex requested a review from adriangb March 4, 2023 19:23
Copy link
Member

@adriangb adriangb left a comment

Choose a reason for hiding this comment

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

server_supports_state = state is not None
if lifespan_needs_state and not server_supports_state:
raise RuntimeError(
'The server does not support "state" in the lifespan scope.'
Copy link
Member Author

Choose a reason for hiding this comment

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

I removed the "please change server", and use the created the state variable to save one line below... 😬

@Kludex Kludex merged commit da6461b into master Mar 5, 2023
@Kludex Kludex deleted the state-map branch March 5, 2023 14:49
@Kludex Kludex mentioned this pull request Mar 5, 2023
@adriangb
Copy link
Member

adriangb commented Mar 5, 2023

Before we release this, just wondering if this API might be better:

Lifespan = Callable[[Starlette], AsyncContextManager[None | Dict[str, Any]]]

class MyStateType(TypedDict):
    client: httpx.AsyncClient

@asynccontextmanager
async def lifespan(app: Starlette) -> MyStateType:
    async with httpx.AsyncClient() as client:
        yield {"client": client}

I think it would also be even less changes in Starlette since we could do:

async with lifespan as maybe_state:
    if maybe_state:
        scope["state"].update(maybe_state)

And not have to introspect the function signature or anything.

@Kludex
Copy link
Member Author

Kludex commented Mar 5, 2023

We'd still have to introspect in the startup and shutdown, no?

@adriangb
Copy link
Member

adriangb commented Mar 5, 2023

Not sure how to handle shutdown. startup would be easy: just let it return None | Dict[str, Any]. But I'd also be okay only supporting this for context manager lifespans.

@Kludex
Copy link
Member Author

Kludex commented Mar 5, 2023

My question was more about what you said:

I think it would also be even less changes in Starlette since we could do:

And not have to introspect the function signature or anything.

Considering that, we'd still need to introspect for shutdown, at least.

That said... I think what you propose looks better for lifespan, but I'm not sure if it's worth it... Do you want to create a PR, and we discuss there?

adriangb added a commit to adriangb/starlette that referenced this pull request Mar 5, 2023
Kludex pushed a commit that referenced this pull request Mar 9, 2023
Kludex added a commit that referenced this pull request Mar 9, 2023
* Revert "Support lifespan state (#2060)"

This reverts commit da6461b.

* new implementation

* Deprecate `on_startup` and `on_shutdown` events

* Rename `events.md` by `lifespan.md`

---------

Co-authored-by: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>
@empicano
Copy link

I discovered this yesterday after upgrading from an older version and just wanted to say that I'm delighted by it. It's so much nicer than using globals! Thanks for the work that went into 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