mirror of https://github.com/tiangolo/fastapi.git
feat: lifespan dependency scope (applications, deps, params)
- Wire lifespan-scoped deps in applications.py via _wrap_lifespan_with_dependency_cache - Extend Depends(..., scope=...) with 'lifespan' in params/models/utils - Resolved with merge of origin/master Made-with: Cursor
This commit is contained in:
parent
9c380fe3d9
commit
df44ff0d9e
|
|
@ -1,4 +1,5 @@
|
||||||
from collections.abc import Awaitable, Callable, Coroutine, Sequence
|
from collections.abc import Awaitable, Callable, Coroutine, Sequence
|
||||||
|
from contextlib import AsyncExitStack, asynccontextmanager
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Annotated, Any, TypeVar
|
from typing import Annotated, Any, TypeVar
|
||||||
|
|
||||||
|
|
@ -37,6 +38,43 @@ from typing_extensions import deprecated
|
||||||
|
|
||||||
AppType = TypeVar("AppType", bound="FastAPI")
|
AppType = TypeVar("AppType", bound="FastAPI")
|
||||||
|
|
||||||
|
# Attribute name on the router used to run lifespan-scoped dependencies at startup.
|
||||||
|
FASTAPI_LIFESPAN_DEPENDENCY_CACHE = "fastapi_lifespan_dependency_cache"
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_lifespan_with_dependency_cache(original: Any) -> Any:
|
||||||
|
"""Wrap the user's lifespan to run and cache lifespan-scoped dependencies."""
|
||||||
|
|
||||||
|
def wrapped(app: Any) -> Any:
|
||||||
|
@asynccontextmanager
|
||||||
|
async def cm() -> Any:
|
||||||
|
fastapi_app = getattr(app, "_fastapi_app", None)
|
||||||
|
stack: AsyncExitStack | None = None
|
||||||
|
orig_cm = original(app)
|
||||||
|
try:
|
||||||
|
if fastapi_app is not None:
|
||||||
|
stack = AsyncExitStack()
|
||||||
|
await stack.__aenter__()
|
||||||
|
cache: dict[Any, Any] = {}
|
||||||
|
await routing._run_lifespan_dependencies(app, cache, stack)
|
||||||
|
setattr(
|
||||||
|
fastapi_app.state,
|
||||||
|
FASTAPI_LIFESPAN_DEPENDENCY_CACHE,
|
||||||
|
cache,
|
||||||
|
)
|
||||||
|
yield await orig_cm.__aenter__()
|
||||||
|
finally:
|
||||||
|
import sys
|
||||||
|
|
||||||
|
exc_type, exc_val, exc_tb = sys.exc_info()
|
||||||
|
await orig_cm.__aexit__(exc_type, exc_val, exc_tb)
|
||||||
|
if stack is not None:
|
||||||
|
await stack.__aexit__(exc_type, exc_val, exc_tb)
|
||||||
|
|
||||||
|
return cm()
|
||||||
|
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
class FastAPI(Starlette):
|
class FastAPI(Starlette):
|
||||||
"""
|
"""
|
||||||
|
|
@ -979,13 +1017,19 @@ class FastAPI(Starlette):
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
] = {}
|
] = {}
|
||||||
|
_inner_lifespan = (
|
||||||
|
lifespan
|
||||||
|
if lifespan is not None
|
||||||
|
else (lambda app: routing._DefaultLifespan(app))
|
||||||
|
)
|
||||||
|
_lifespan = _wrap_lifespan_with_dependency_cache(_inner_lifespan)
|
||||||
self.router: routing.APIRouter = routing.APIRouter(
|
self.router: routing.APIRouter = routing.APIRouter(
|
||||||
routes=routes,
|
routes=routes,
|
||||||
redirect_slashes=redirect_slashes,
|
redirect_slashes=redirect_slashes,
|
||||||
dependency_overrides_provider=self,
|
dependency_overrides_provider=self,
|
||||||
on_startup=on_startup,
|
on_startup=on_startup,
|
||||||
on_shutdown=on_shutdown,
|
on_shutdown=on_shutdown,
|
||||||
lifespan=lifespan,
|
lifespan=_lifespan,
|
||||||
default_response_class=default_response_class,
|
default_response_class=default_response_class,
|
||||||
dependencies=dependencies,
|
dependencies=dependencies,
|
||||||
callbacks=callbacks,
|
callbacks=callbacks,
|
||||||
|
|
@ -995,6 +1039,7 @@ class FastAPI(Starlette):
|
||||||
generate_unique_id_function=generate_unique_id_function,
|
generate_unique_id_function=generate_unique_id_function,
|
||||||
strict_content_type=strict_content_type,
|
strict_content_type=strict_content_type,
|
||||||
)
|
)
|
||||||
|
self.router._fastapi_app = self
|
||||||
self.exception_handlers: dict[
|
self.exception_handlers: dict[
|
||||||
Any, Callable[[Request, Any], Response | Awaitable[Response]]
|
Any, Callable[[Request, Any], Response | Awaitable[Response]]
|
||||||
] = {} if exception_handlers is None else dict(exception_handlers)
|
] = {} if exception_handlers is None else dict(exception_handlers)
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ class Dependant:
|
||||||
parent_oauth_scopes: list[str] | None = None
|
parent_oauth_scopes: list[str] | None = None
|
||||||
use_cache: bool = True
|
use_cache: bool = True
|
||||||
path: str | None = None
|
path: str | None = None
|
||||||
scope: Literal["function", "request"] | None = None
|
scope: Literal["function", "request", "lifespan"] | None = None
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def oauth_scopes(self) -> list[str]:
|
def oauth_scopes(self) -> list[str]:
|
||||||
|
|
|
||||||
|
|
@ -291,7 +291,7 @@ def get_dependant(
|
||||||
own_oauth_scopes: list[str] | None = None,
|
own_oauth_scopes: list[str] | None = None,
|
||||||
parent_oauth_scopes: list[str] | None = None,
|
parent_oauth_scopes: list[str] | None = None,
|
||||||
use_cache: bool = True,
|
use_cache: bool = True,
|
||||||
scope: Literal["function", "request"] | None = None,
|
scope: Literal["function", "request", "lifespan"] | None = None,
|
||||||
) -> Dependant:
|
) -> Dependant:
|
||||||
dependant = Dependant(
|
dependant = Dependant(
|
||||||
call=call,
|
call=call,
|
||||||
|
|
@ -327,6 +327,17 @@ def get_dependant(
|
||||||
f'The dependency "{call_name}" has a scope of '
|
f'The dependency "{call_name}" has a scope of '
|
||||||
'"request", it cannot depend on dependencies with scope "function".'
|
'"request", it cannot depend on dependencies with scope "function".'
|
||||||
)
|
)
|
||||||
|
# Lifespan-scoped dependencies can only depend on other lifespan-scoped deps.
|
||||||
|
if dependant.computed_scope == "lifespan" and param_details.depends.scope not in (
|
||||||
|
None,
|
||||||
|
"lifespan",
|
||||||
|
):
|
||||||
|
assert dependant.call
|
||||||
|
raise DependencyScopeError(
|
||||||
|
f'The dependency "{dependant.call.__name__}" has a scope of '
|
||||||
|
'"lifespan", it cannot depend on dependencies with scope '
|
||||||
|
f'"{param_details.depends.scope}".'
|
||||||
|
)
|
||||||
sub_own_oauth_scopes: list[str] = []
|
sub_own_oauth_scopes: list[str] = []
|
||||||
if isinstance(param_details.depends, params.Security):
|
if isinstance(param_details.depends, params.Security):
|
||||||
if param_details.depends.scopes:
|
if param_details.depends.scopes:
|
||||||
|
|
@ -608,6 +619,7 @@ async def solve_dependencies(
|
||||||
# people might be monkey patching this function (although that's not supported)
|
# people might be monkey patching this function (although that's not supported)
|
||||||
async_exit_stack: AsyncExitStack,
|
async_exit_stack: AsyncExitStack,
|
||||||
embed_body_fields: bool,
|
embed_body_fields: bool,
|
||||||
|
solving_lifespan_deps: bool = False,
|
||||||
) -> SolvedDependency:
|
) -> SolvedDependency:
|
||||||
request_astack = request.scope.get("fastapi_inner_astack")
|
request_astack = request.scope.get("fastapi_inner_astack")
|
||||||
assert isinstance(request_astack, AsyncExitStack), (
|
assert isinstance(request_astack, AsyncExitStack), (
|
||||||
|
|
@ -656,6 +668,7 @@ async def solve_dependencies(
|
||||||
dependency_cache=dependency_cache,
|
dependency_cache=dependency_cache,
|
||||||
async_exit_stack=async_exit_stack,
|
async_exit_stack=async_exit_stack,
|
||||||
embed_body_fields=embed_body_fields,
|
embed_body_fields=embed_body_fields,
|
||||||
|
solving_lifespan_deps=solving_lifespan_deps,
|
||||||
)
|
)
|
||||||
background_tasks = solved_result.background_tasks
|
background_tasks = solved_result.background_tasks
|
||||||
if solved_result.errors:
|
if solved_result.errors:
|
||||||
|
|
@ -663,6 +676,30 @@ async def solve_dependencies(
|
||||||
continue
|
continue
|
||||||
if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache:
|
if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache:
|
||||||
solved = dependency_cache[sub_dependant.cache_key]
|
solved = dependency_cache[sub_dependant.cache_key]
|
||||||
|
elif sub_dependant.computed_scope == "lifespan":
|
||||||
|
# At request time, lifespan deps must come from cache (set at startup).
|
||||||
|
if sub_dependant.cache_key in dependency_cache:
|
||||||
|
solved = dependency_cache[sub_dependant.cache_key]
|
||||||
|
elif solving_lifespan_deps:
|
||||||
|
# At startup: run the lifespan dep; request_astack is the lifespan stack.
|
||||||
|
if (
|
||||||
|
use_sub_dependant.is_gen_callable
|
||||||
|
or use_sub_dependant.is_async_gen_callable
|
||||||
|
):
|
||||||
|
solved = await _solve_generator(
|
||||||
|
dependant=use_sub_dependant,
|
||||||
|
stack=request_astack,
|
||||||
|
sub_values=solved_result.values,
|
||||||
|
)
|
||||||
|
elif use_sub_dependant.is_coroutine_callable:
|
||||||
|
solved = await call(**solved_result.values)
|
||||||
|
else:
|
||||||
|
solved = await run_in_threadpool(call, **solved_result.values)
|
||||||
|
else:
|
||||||
|
raise DependencyScopeError(
|
||||||
|
"Lifespan-scoped dependency was not initialized at application startup. "
|
||||||
|
"Ensure the application lifespan runs and populates lifespan dependencies."
|
||||||
|
)
|
||||||
elif (
|
elif (
|
||||||
use_sub_dependant.is_gen_callable or use_sub_dependant.is_async_gen_callable
|
use_sub_dependant.is_gen_callable or use_sub_dependant.is_async_gen_callable
|
||||||
):
|
):
|
||||||
|
|
|
||||||
|
|
@ -2314,7 +2314,7 @@ def Depends( # noqa: N802
|
||||||
),
|
),
|
||||||
] = True,
|
] = True,
|
||||||
scope: Annotated[
|
scope: Annotated[
|
||||||
Literal["function", "request"] | None,
|
Literal["function", "request", "lifespan"] | None,
|
||||||
Doc(
|
Doc(
|
||||||
"""
|
"""
|
||||||
Mainly for dependencies with `yield`, define when the dependency function
|
Mainly for dependencies with `yield`, define when the dependency function
|
||||||
|
|
@ -2330,6 +2330,10 @@ def Depends( # noqa: N802
|
||||||
that handles the request (similar to when using `"function"`), but end
|
that handles the request (similar to when using `"function"`), but end
|
||||||
**after** the response is sent back to the client. So, the dependency
|
**after** the response is sent back to the client. So, the dependency
|
||||||
function will be executed **around** the **request** and response cycle.
|
function will be executed **around** the **request** and response cycle.
|
||||||
|
* `"lifespan"`: the dependency is evaluated **once** when the application
|
||||||
|
starts and the same value is reused for every request. It is cleaned up
|
||||||
|
when the application shuts down. Use this for resources like database
|
||||||
|
connection pools that should live for the application lifetime.
|
||||||
|
|
||||||
Read more about it in the
|
Read more about it in the
|
||||||
[FastAPI docs for FastAPI Dependencies with yield](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/#early-exit-and-scope)
|
[FastAPI docs for FastAPI Dependencies with yield](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/#early-exit-and-scope)
|
||||||
|
|
|
||||||
|
|
@ -746,7 +746,7 @@ class File(Form): # type: ignore[misc] # ty: ignore[unused-ignore-comment]
|
||||||
class Depends:
|
class Depends:
|
||||||
dependency: Callable[..., Any] | None = None
|
dependency: Callable[..., Any] | None = None
|
||||||
use_cache: bool = True
|
use_cache: bool = True
|
||||||
scope: Literal["function", "request"] | None = None
|
scope: Literal["function", "request", "lifespan"] | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue