From f1b53236a9494ed329a129971b7b61b90c061aee Mon Sep 17 00:00:00 2001 From: Nir Schulman Date: Sat, 8 Nov 2025 13:01:57 +0200 Subject: [PATCH] Updated the lifespan-scoped-dependencies tutorial and fixed some coverage issues. --- .../lifespan-scoped-dependencies.md | 69 +++++++++---------- tests/test_router_events.py | 38 ++++++++++ 2 files changed, 71 insertions(+), 36 deletions(-) diff --git a/docs/en/docs/tutorial/dependencies/lifespan-scoped-dependencies.md b/docs/en/docs/tutorial/dependencies/lifespan-scoped-dependencies.md index ba46da330..1c768ab48 100644 --- a/docs/en/docs/tutorial/dependencies/lifespan-scoped-dependencies.md +++ b/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`. diff --git a/tests/test_router_events.py b/tests/test_router_events.py index 2f110e684..ee953102b 100644 --- a/tests/test_router_events.py +++ b/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