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
So far we've used dependencies which are "endpoint scoped". Meaning, they are
called again and again for every incoming request to the endpoint. However,
So far we've used dependencies which are evaluated once for every incoming request. However,
this is not always ideal:
* 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
less efficient when working with lots of connections and would prefer that
clients would create a single connection for their operations.
less efficient when working with lots of connections and would prefer to have connections
re-used across different operations.
For such cases can be solved by using "lifespan scoped dependencies".
## What is a lifespan scoped dependency?
Lifespan scoped dependencies work similarly to the (endpoint scoped)
dependencies we've worked with so far. However, unlike endpoint scoped
dependencies, lifespan scoped dependencies are called once and only
once in the application's lifespan:
dependencies we've worked with so far except for the following differences:
* During the application startup process, all lifespan scoped dependencies will
be called.
* Their returned value will be shared across all requests to the application.
* During the application's shutdown process, all lifespan scoped dependencies
will be gracefully teared down.
* Lifespan scoped dependencies will only be called once, during the application's startup process.
* Lifespan scoped dependencies will share their values amongst all requests the application receives.
* Lifespan scoped dependencies will only be cleaned during the application's shutdown process.
## Create a lifespan scoped dependency
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 *}
@ -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
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
`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).
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 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
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 *}
In this example, the `read_users` and `read_groups` endpoints are using
`use_cache=False` whereas the `read_items` and `read_item` are using
`use_cache=True`.
That means that we'll have a total of 3 connections created
for the duration of the application's lifespan:
In this example, we used a lifespan scoped dependency in a total of 4 places:
* The `read_item` endpoint (with `use_cache=True`)
* The `read_items` endpoint (with `use_cache=True`)
* 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.
* 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.
@ -85,27 +89,20 @@ Endpoint scoped dependencies may use lifespan scoped sub dependencies as well:
{* ../../docs_src/dependencies/tutorial013d_an_py39.py *}
/// note
You can pass `dependency_scope="endpoint"` if you wish to explicitly specify
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.
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
`get_database_connection`, which allows us to re-use the same connection each time the dependency is called.
## Dependency Scope Conflicts
By definition, lifespan scoped dependencies are being setup in the application's
startup process, before any request is ever being made to any endpoint.
Therefore, it is not possible for a lifespan scoped dependency to use any
parameters that require the scope of an endpoint.
startup process, before any request is ever being made to an endpoint.
Therefore, it doesn't make sense for a lifespan scoped dependency to use any
parameters that only make sense in the context of an endpoint.
That includes but not limited to:
* Parts of the request (like `Body`, `Query` and `Path`)
* 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):
app_startup: 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_shutdown: bool = False
sub_router_startup: bool = False
@ -39,6 +45,26 @@ def test_router_events(state: State) -> None:
def app_shutdown() -> None:
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.on_event("startup")
@ -63,25 +89,37 @@ def test_router_events(state: State) -> None:
app.include_router(router)
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.sub_router_startup 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.sub_router_shutdown is False
with TestClient(app) as client:
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.sub_router_startup is True
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.sub_router_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_class_startup is True
assert state.app_instance_startup is True
assert state.router_startup is True
assert state.sub_router_startup 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.sub_router_shutdown is True

Loading…
Cancel
Save