Browse Source

Updated the lifespan-scoped-dependencies tutorial and fixed some coverage issues.

pull/12529/head
Nir Schulman 8 months ago
parent
commit
f1b53236a9
  1. 69
      docs/en/docs/tutorial/dependencies/lifespan-scoped-dependencies.md
  2. 38
      tests/test_router_events.py

69
docs/en/docs/tutorial/dependencies/lifespan-scoped-dependencies.md

@ -2,8 +2,7 @@
## Intro ## Intro
So far we've used dependencies which are "endpoint scoped". Meaning, they are So far we've used dependencies which are evaluated once for every incoming request. However,
called again and again for every incoming request to the endpoint. However,
this is not always ideal: this is not always ideal:
* Sometimes dependencies have a large setup/teardown time. Running it for every request will result in bad performance. * Sometimes dependencies have a large setup/teardown time. Running it for every request will result in bad performance.
@ -12,29 +11,25 @@ of the application between multiple requests.
An example of this would be a connection to a database. Databases are typically An example of this would be a connection to a database. Databases are typically
less efficient when working with lots of connections and would prefer that less efficient when working with lots of connections and would prefer to have connections
clients would create a single connection for their operations. re-used across different operations.
For such cases can be solved by using "lifespan scoped dependencies". For such cases can be solved by using "lifespan scoped dependencies".
## What is a lifespan scoped dependency? ## What is a lifespan scoped dependency?
Lifespan scoped dependencies work similarly to the (endpoint scoped) Lifespan scoped dependencies work similarly to the (endpoint scoped)
dependencies we've worked with so far. However, unlike endpoint scoped dependencies we've worked with so far except for the following differences:
dependencies, lifespan scoped dependencies are called once and only
once in the application's lifespan:
* During the application startup process, all lifespan scoped dependencies will * Lifespan scoped dependencies will only be called once, during the application's startup process.
be called. * Lifespan scoped dependencies will share their values amongst all requests the application receives.
* Their returned value will be shared across all requests to the application. * Lifespan scoped dependencies will only be cleaned during the application's shutdown process.
* During the application's shutdown process, all lifespan scoped dependencies
will be gracefully teared down.
## Create a lifespan scoped dependency ## Create a lifespan scoped dependency
You may declare a dependency as a lifespan scoped dependency by passing You may declare a dependency as a lifespan scoped dependency by passing
`dependency_scope="lifespan"` to the `Depends` function: `scope="lifespan"` to the `Depends` function:
{* ../../docs_src/dependencies/tutorial013a_an_py39.py *} {* ../../docs_src/dependencies/tutorial013a_an_py39.py *}
@ -44,8 +39,10 @@ In the example above we saved the annotation to a separate variable, and then
reused it in our endpoints. This is not a requirement, we could also declare reused it in our endpoints. This is not a requirement, we could also declare
the exact same annotation in both endpoints. However, it is recommended that you the exact same annotation in both endpoints. However, it is recommended that you
do save the annotation to a variable so you won't accidentally forget to pass do save the annotation to a variable so you won't accidentally forget to pass
`dependency_scope="lifespan"` to some of the endpoints (Causing the endpoint `scope="lifespan"` to some of the endpoints (Causing the endpoint
to create a new database connection for every request). to create a new database connection for every request).
It will also be more intuitive to use the exact same annotation, reminding us that the
we are using the exact same value across all endpoints.
/// ///
@ -58,18 +55,25 @@ shuts down, **FastAPI** will make sure to gracefully close the connection object
## The `use_cache` argument ## The `use_cache` argument
The `use_cache` argument works similarly to the way it worked with endpoint The `use_cache` argument works similarly to the way it worked with endpoint
scoped dependencies. Meaning as **FastAPI** gathers lifespan scoped dependencies, it scoped dependencies. Meaning, as **FastAPI** gathers lifespan scoped dependencies, it
will cache dependencies it already encountered before. However, you can disable will cache dependencies it already encountered before. However, you can disable
this behavior by passing `use_cache=False` to `Depends`: this behavior by passing `use_cache=False` to `Depends`. This will cause a new lifespan
dependency to be created for every endpoint/dependency/router where it shows up:
{* ../../docs_src/dependencies/tutorial013b_an_py39.py *} {* ../../docs_src/dependencies/tutorial013b_an_py39.py *}
In this example, the `read_users` and `read_groups` endpoints are using In this example, we used a lifespan scoped dependency in a total of 4 places:
`use_cache=False` whereas the `read_items` and `read_item` are using
`use_cache=True`. * The `read_item` endpoint (with `use_cache=True`)
That means that we'll have a total of 3 connections created * The `read_items` endpoint (with `use_cache=True`)
for the duration of the application's lifespan: * The `read_users` endpoint (with `use_cache=False`)
* The `read_groups` endpoint (with `use_cache=False`)
Since the `read_item` and `read_items` endpoints enabled the cache, they will use the same connection.
However, since the `read_user` and `read_groups` disabled the cache, each of them will receive a new,
dedicated connection for the application lifespan.
In total, we will have 3 connections created for our application which will remain for its entire lifespan:
* One connection will be shared across all requests for the `read_items` and `read_item` endpoints. * One connection will be shared across all requests for the `read_items` and `read_item` endpoints.
* A second connection will be shared across all requests for the `read_users` endpoint. * A second connection will be shared across all requests for the `read_users` endpoint.
* A third and final connection will be shared across all requests for the `read_groups` endpoint. * A third and final connection will be shared across all requests for the `read_groups` endpoint.
@ -85,27 +89,20 @@ Endpoint scoped dependencies may use lifespan scoped sub dependencies as well:
{* ../../docs_src/dependencies/tutorial013d_an_py39.py *} {* ../../docs_src/dependencies/tutorial013d_an_py39.py *}
/// note Here, we defined an endpoint-scoped dependency called `get_user_record` which will fetch
the information of the given user from the database. It uses a lifespan-scoped sub-dependency called
You can pass `dependency_scope="endpoint"` if you wish to explicitly specify `get_database_connection`, which allows us to re-use the same connection each time the dependency is called.
that a dependency is endpoint scoped. It will work the same as not specifying
a dependency scope at all.
///
As you can see, regardless of the scope, dependencies can use lifespan scoped
sub-dependencies.
## Dependency Scope Conflicts ## Dependency Scope Conflicts
By definition, lifespan scoped dependencies are being setup in the application's By definition, lifespan scoped dependencies are being setup in the application's
startup process, before any request is ever being made to any endpoint. startup process, before any request is ever being made to an endpoint.
Therefore, it is not possible for a lifespan scoped dependency to use any Therefore, it doesn't make sense for a lifespan scoped dependency to use any
parameters that require the scope of an endpoint. parameters that only make sense in the context of an endpoint.
That includes but not limited to: That includes but not limited to:
* Parts of the request (like `Body`, `Query` and `Path`) * Parts of the request (like `Body`, `Query` and `Path`)
* The request/response objects themselves (like `Request`, `Response` and `WebSocket`) * The request/response objects themselves (like `Request`, `Response` and `WebSocket`)
* Endpoint scoped sub-dependencies. * Sub-dependencies with smaller scopes (Like `"request"` or `"function"`).
Defining a dependency with such parameters will raise an `InvalidDependencyScope` error. Defining a dependency with such parameters will raise a `DependencyScopeError`.

38
tests/test_router_events.py

@ -10,6 +10,12 @@ from pydantic import BaseModel
class State(BaseModel): class State(BaseModel):
app_startup: bool = False app_startup: bool = False
app_shutdown: bool = False app_shutdown: bool = False
app_class_startup: bool = False
app_instance_startup: bool = False
app_instance_shutdown: bool = False
app_class_shutdown: bool = False
app_call_protocol_startup: bool = False
app_call_protocol_shutdown: bool = False
router_startup: bool = False router_startup: bool = False
router_shutdown: bool = False router_shutdown: bool = False
sub_router_startup: bool = False sub_router_startup: bool = False
@ -39,6 +45,26 @@ def test_router_events(state: State) -> None:
def app_shutdown() -> None: def app_shutdown() -> None:
state.app_shutdown = True state.app_shutdown = True
@app.on_event("startup")
class AppStartup:
def __init__(self) -> None:
state.app_class_startup = True
def __call__(self):
state.app_instance_startup = True
app.on_event("startup")(AppStartup.__new__(AppStartup))
@app.on_event("shutdown")
class AppShutdown:
def __init__(self):
state.app_class_shutdown = True
def __call__(self):
state.app_instance_shutdown = True
app.on_event("shutdown")(AppShutdown.__new__(AppShutdown))
router = APIRouter() router = APIRouter()
@router.on_event("startup") @router.on_event("startup")
@ -63,25 +89,37 @@ def test_router_events(state: State) -> None:
app.include_router(router) app.include_router(router)
assert state.app_startup is False assert state.app_startup is False
assert state.app_class_startup is False
assert state.app_instance_startup is False
assert state.router_startup is False assert state.router_startup is False
assert state.sub_router_startup is False assert state.sub_router_startup is False
assert state.app_shutdown is False assert state.app_shutdown is False
assert state.app_class_shutdown is False
assert state.app_instance_shutdown is False
assert state.router_shutdown is False assert state.router_shutdown is False
assert state.sub_router_shutdown is False assert state.sub_router_shutdown is False
with TestClient(app) as client: with TestClient(app) as client:
assert state.app_startup is True assert state.app_startup is True
assert state.app_class_startup is True
assert state.app_instance_startup is True
assert state.router_startup is True assert state.router_startup is True
assert state.sub_router_startup is True assert state.sub_router_startup is True
assert state.app_shutdown is False assert state.app_shutdown is False
assert state.app_class_shutdown is False
assert state.app_instance_shutdown is False
assert state.router_shutdown is False assert state.router_shutdown is False
assert state.sub_router_shutdown is False assert state.sub_router_shutdown is False
response = client.get("/") response = client.get("/")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
assert response.json() == {"message": "Hello World"} assert response.json() == {"message": "Hello World"}
assert state.app_startup is True assert state.app_startup is True
assert state.app_class_startup is True
assert state.app_instance_startup is True
assert state.router_startup is True assert state.router_startup is True
assert state.sub_router_startup is True assert state.sub_router_startup is True
assert state.app_shutdown is True assert state.app_shutdown is True
assert state.app_class_shutdown is True
assert state.app_instance_shutdown is True
assert state.router_shutdown is True assert state.router_shutdown is True
assert state.sub_router_shutdown is True assert state.sub_router_shutdown is True

Loading…
Cancel
Save