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

✨ Add support for lifespan async context managers (superseding startup and shutdown events) #2944

Merged
merged 31 commits into from
Mar 7, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
bb2ec92
Add lifespan context argument
uSpike Mar 13, 2021
4620732
Add tests for lifespan context and refactor router event tests
uSpike Mar 13, 2021
8c33c94
Add lifespan docs
uSpike Mar 13, 2021
ecd6d27
Fix lifespan typing
uSpike Mar 13, 2021
c736405
Fix lifespan doc highlight
uSpike Mar 13, 2021
bc1f5fa
Fix event tests repeated code
uSpike Mar 13, 2021
12e924d
Fix import sorting
uSpike Mar 13, 2021
c78ba44
Merge branch 'master' of github.com:tiangolo/fastapi into add-lifespa…
uSpike Dec 19, 2021
c3a3d25
update lifespan to be an async context manager
meshantz Nov 5, 2021
8b1dfd5
Fix formatting
uSpike Dec 19, 2021
48fdb76
Fix type of lifespan to AsyncContextManager[Any]
uSpike Dec 19, 2021
92897b3
Fix docs for lifespan asynccontextmanager
uSpike Dec 19, 2021
6f9ebea
Add lifespan context argument
uSpike Mar 13, 2021
edef8c0
Add tests for lifespan context and refactor router event tests
uSpike Mar 13, 2021
76968ca
Add lifespan docs
uSpike Mar 13, 2021
0dbcb35
Fix lifespan typing
uSpike Mar 13, 2021
2712fb6
Fix lifespan doc highlight
uSpike Mar 13, 2021
d15df2d
Fix event tests repeated code
uSpike Mar 13, 2021
9665645
Fix import sorting
uSpike Mar 13, 2021
628aa71
update lifespan to be an async context manager
meshantz Nov 5, 2021
f612d74
Fix formatting
uSpike Dec 19, 2021
f13274c
Fix type of lifespan to AsyncContextManager[Any]
uSpike Dec 19, 2021
0eed654
Fix docs for lifespan asynccontextmanager
uSpike Dec 19, 2021
81591ba
Use @asynccontextmanager for lifespan for test
JonathanPlasse Oct 16, 2022
d60f9b8
Use FastAPI instead of Starlette for lifespan
JonathanPlasse Oct 21, 2022
2c05463
Replace lifespan AsyncIterator by AsyncGenerator
JonathanPlasse Oct 21, 2022
5ae6547
🔀 Merge master
tiangolo Mar 6, 2023
0ef7cfa
Merge branch 'master' into add-lifespan-context-v3
tiangolo Mar 6, 2023
8b6dd1a
🔀 Merge PR #5503 from @JonathanPlasse with updated types
tiangolo Mar 6, 2023
c842bdd
📝 Update docs for Lifespan events
tiangolo Mar 7, 2023
6adb554
✅ Add tests for new lifespan event docs
tiangolo Mar 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/en/docs/advanced/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,14 @@ Here, the `shutdown` event handler function will write a text line `"Application

!!! info
You can read more about these event handlers in <a href="https://www.starlette.io/events/" class="external-link" target="_blank">Starlette's Events' docs</a>.

# Lifespan

You can also define a lifespan context as an asynchronous context manager, instead of using separate startup and shutdown functions.

This `async` function must be declared with the `@asynccontextmanager` decorator. This was added to `contextlib` in Python 3.7. For earlier versions of Python,
you can install the `contextlib2` library to get `@asynccontextmanager`.

```Python hl_lines="4"
{!../../../docs_src/events/tutorial003.py!}
```
18 changes: 18 additions & 0 deletions docs_src/events/tutorial003.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from contextlib import asynccontextmanager

from fastapi import FastAPI


@asynccontextmanager
async def lifespan(app):
print("startup")
yield
print("shutdown")


app = FastAPI(lifespan=lifespan)


@app.get("/")
async def hello():
return {"result": "hello world"}
15 changes: 14 additions & 1 deletion fastapi/applications.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
from typing import Any, Callable, Coroutine, Dict, List, Optional, Sequence, Type, Union
from typing import (
Any,
AsyncContextManager,
Callable,
Coroutine,
Dict,
List,
Optional,
Sequence,
Type,
Union,
)

from fastapi import routing
from fastapi.concurrency import AsyncExitStack
Expand Down Expand Up @@ -55,6 +66,7 @@ def __init__(
] = None,
on_startup: Optional[Sequence[Callable[[], Any]]] = None,
on_shutdown: Optional[Sequence[Callable[[], Any]]] = None,
lifespan: Optional[Callable[[Starlette], AsyncContextManager[Any]]] = None,
terms_of_service: Optional[str] = None,
contact: Optional[Dict[str, Union[str, Any]]] = None,
license_info: Optional[Dict[str, Union[str, Any]]] = None,
Expand All @@ -74,6 +86,7 @@ def __init__(
dependency_overrides_provider=self,
on_startup=on_startup,
on_shutdown=on_shutdown,
lifespan=lifespan,
default_response_class=default_response_class,
dependencies=dependencies,
callbacks=callbacks,
Expand Down
4 changes: 4 additions & 0 deletions fastapi/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
from typing import (
Any,
AsyncContextManager,
Callable,
Coroutine,
Dict,
Expand Down Expand Up @@ -40,6 +41,7 @@
from pydantic.error_wrappers import ErrorWrapper, ValidationError
from pydantic.fields import ModelField, Undefined
from starlette import routing
from starlette.applications import Starlette
from starlette.concurrency import run_in_threadpool
from starlette.exceptions import HTTPException
from starlette.requests import Request
Expand Down Expand Up @@ -450,6 +452,7 @@ def __init__(
route_class: Type[APIRoute] = APIRoute,
on_startup: Optional[Sequence[Callable[[], Any]]] = None,
on_shutdown: Optional[Sequence[Callable[[], Any]]] = None,
lifespan: Optional[Callable[[Starlette], AsyncContextManager[Any]]] = None,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
) -> None:
Expand All @@ -459,6 +462,7 @@ def __init__(
default=default, # type: ignore # in Starlette
on_startup=on_startup, # type: ignore # in Starlette
on_shutdown=on_shutdown, # type: ignore # in Starlette
lifespan=lifespan, # type: ignore # in Starlette
)
if prefix:
assert prefix.startswith("/"), "A path prefix must start with '/'"
Expand Down
98 changes: 60 additions & 38 deletions tests/test_router_events.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pytest
from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
Expand All @@ -12,57 +13,49 @@ class State(BaseModel):
sub_router_shutdown: bool = False


state = State()
@pytest.fixture
def state():
return State()

app = FastAPI()

def test_router_events(state):
app = FastAPI()

@app.on_event("startup")
def app_startup():
state.app_startup = True
@app.get("/")
def main():
return {"message": "Hello World"}

@app.on_event("startup")
def app_startup():
state.app_startup = True

@app.on_event("shutdown")
def app_shutdown():
state.app_shutdown = True
@app.on_event("shutdown")
def app_shutdown():
state.app_shutdown = True

router = APIRouter()

router = APIRouter()
@router.on_event("startup")
def router_startup():
state.router_startup = True

@router.on_event("shutdown")
def router_shutdown():
state.router_shutdown = True

@router.on_event("startup")
def router_startup():
state.router_startup = True
sub_router = APIRouter()

@sub_router.on_event("startup")
def sub_router_startup():
state.sub_router_startup = True

@router.on_event("shutdown")
def router_shutdown():
state.router_shutdown = True
@sub_router.on_event("shutdown")
def sub_router_shutdown():
state.sub_router_shutdown = True

router.include_router(sub_router)
app.include_router(router)

sub_router = APIRouter()


@sub_router.on_event("startup")
def sub_router_startup():
state.sub_router_startup = True


@sub_router.on_event("shutdown")
def sub_router_shutdown():
state.sub_router_shutdown = True


@sub_router.get("/")
def main():
return {"message": "Hello World"}


router.include_router(sub_router)
app.include_router(router)


def test_router_events():
assert state.app_startup is False
assert state.router_startup is False
assert state.sub_router_startup is False
Expand All @@ -85,3 +78,32 @@ def test_router_events():
assert state.app_shutdown is True
assert state.router_shutdown is True
assert state.sub_router_shutdown is True


def test_app_lifespan_state(state):
class Lifespan:
def __init__(self, app):
pass

async def __aenter__(self):
state.app_startup = True

async def __aexit__(self, exc_type, exc_value, exc_tb):
state.app_shutdown = True

app = FastAPI(lifespan=Lifespan)

@app.get("/")
def main():
return {"message": "Hello World"}

assert state.app_startup is False
assert state.app_shutdown is False
with TestClient(app) as client:
assert state.app_startup is True
assert state.app_shutdown is False
response = client.get("/")
assert response.status_code == 200, response.text
assert response.json() == {"message": "Hello World"}
assert state.app_startup is True
assert state.app_shutdown is True