From b123c5f2ef36b721e08ebd8d8e75a50221c0b5e7 Mon Sep 17 00:00:00 2001 From: Fodor Zoltan Date: Sun, 21 Sep 2025 13:39:14 +0300 Subject: [PATCH] Set dependency parallelization default opt in. (#639) --- fastapi/applications.py | 11 +++++++++++ fastapi/dependencies/models.py | 2 +- fastapi/dependencies/utils.py | 23 ++++++++++++++++++----- fastapi/param_functions.py | 33 +++++++++++++++++++++++++++++++-- fastapi/params.py | 5 +++-- tests/test_dependency_cache.py | 2 +- 6 files changed, 65 insertions(+), 11 deletions(-) diff --git a/fastapi/applications.py b/fastapi/applications.py index b3424efcc..d34e60bb8 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -836,6 +836,16 @@ class FastAPI(Starlette): """ ), ] = None, + depends_default_parallelizable: Annotated[ + bool, + Doc( + """ + Default for whether dependencies are allowed to run in parallel when not + explicitly specified per dependency. Projects can opt in by setting this + to True when creating the application. The default is False. + """ + ), + ] = False, **extra: Annotated[ Any, Doc( @@ -865,6 +875,7 @@ class FastAPI(Starlette): self.servers = servers or [] self.separate_input_output_schemas = separate_input_output_schemas self.openapi_external_docs = openapi_external_docs + self.depends_default_parallelizable = depends_default_parallelizable self.extra = extra self.openapi_version: Annotated[ str, diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index 614910308..75866ff84 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -32,7 +32,7 @@ class Dependant: use_cache: bool = True path: Optional[str] = None cache_key: Tuple[Optional[Callable[..., Any]], Tuple[str, ...]] = field(init=False) - parallelizable: bool = True + parallelizable: Optional[bool] = None def __post_init__(self) -> None: self.cache_key = (self.call, tuple(sorted(set(self.security_scopes or [])))) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index bcf355f2d..b8c8153de 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -281,7 +281,7 @@ def get_dependant( name: Optional[str] = None, security_scopes: Optional[List[str]] = None, use_cache: bool = True, - parallelizable: bool = True, + parallelizable: Optional[bool] = None, ) -> Dependant: path_param_names = get_path_param_names(path) endpoint_signature = get_typed_signature(call) @@ -589,13 +589,19 @@ class DependencySolveException(Exception): self.errors = errors -def is_context_sensitive(dependant: Dependant) -> bool: - if dependant.parallelizable is False or ( +def is_context_sensitive(dependant: Dependant, *, default_parallelizable: bool) -> bool: + effective_parallelizable = ( + default_parallelizable if dependant.parallelizable is None else dependant.parallelizable + ) + if effective_parallelizable is False or ( dependant.call is not None and (is_gen_callable(dependant.call) or is_async_gen_callable(dependant.call)) ): return True - return any(is_context_sensitive(sub) for sub in dependant.dependencies) + return any( + is_context_sensitive(sub, default_parallelizable=default_parallelizable) + for sub in dependant.dependencies + ) def is_context_with_background_task(dependant: Dependant) -> bool: @@ -659,6 +665,7 @@ async def solve_dependencies( call=call, name=sub_dependant.name, security_scopes=sub_dependant.security_scopes, + parallelizable=sub_dependant.parallelizable, ) def resolve_cached() -> Optional["Future[Any]"]: @@ -750,10 +757,16 @@ async def solve_dependencies( if name is not None: values[name] = value + + app = request.app # type: ignore[attr-defined] + default_parallelizable = bool( + getattr(app, "depends_default_parallelizable", False) + ) + sequential_deps = [] parallel_deps = [] for sub in dependant.dependencies: - if is_context_sensitive(sub): + if is_context_sensitive(sub, default_parallelizable=default_parallelizable): sequential_deps.append(sub) else: parallel_deps.append(sub) diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index b3621626c..7433313d3 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -2244,6 +2244,17 @@ def Depends( # noqa: N802 """ ), ] = True, + parallelizable: Annotated[ + Optional[bool], + Doc( + """ + Whether this dependency may be resolved in parallel. + + If not provided, the application-wide default will be used (configured with + `FastAPI(depends_default_parallelizable=...)`). + """ + ), + ] = None, ) -> Any: """ Declare a FastAPI dependency. @@ -2274,7 +2285,9 @@ def Depends( # noqa: N802 return commons ``` """ - return params.Depends(dependency=dependency, use_cache=use_cache) + return params.Depends( + dependency=dependency, use_cache=use_cache, parallelizable=parallelizable + ) def Security( # noqa: N802 @@ -2321,6 +2334,17 @@ def Security( # noqa: N802 """ ), ] = True, + parallelizable: Annotated[ + Optional[bool], + Doc( + """ + Whether this dependency may be resolved in parallel. + + If not provided, the application-wide default will be used (configured with + `FastAPI(depends_default_parallelizable=...)`). + """ + ), + ] = None, ) -> Any: """ Declare a FastAPI Security dependency. @@ -2357,4 +2381,9 @@ def Security( # noqa: N802 return [{"item_id": "Foo", "owner": current_user.username}] ``` """ - return params.Security(dependency=dependency, scopes=scopes, use_cache=use_cache) + return params.Security( + dependency=dependency, + scopes=scopes, + use_cache=use_cache, + parallelizable=parallelizable, + ) diff --git a/fastapi/params.py b/fastapi/params.py index 17c3642b3..55675fa12 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -767,10 +767,11 @@ class Depends: dependency: Optional[Callable[..., Any]] = None, *, use_cache: bool = True, - parallelizable: bool = True, + parallelizable: Optional[bool] = None, ): self.dependency = dependency self.use_cache = use_cache + # None means: use application default at resolution time self.parallelizable = parallelizable def __repr__(self) -> str: @@ -786,7 +787,7 @@ class Security(Depends): *, scopes: Optional[Sequence[str]] = None, use_cache: bool = True, - parallelizable: bool = True, + parallelizable: Optional[bool] = None, ): super().__init__( dependency=dependency, use_cache=use_cache, parallelizable=parallelizable diff --git a/tests/test_dependency_cache.py b/tests/test_dependency_cache.py index 9d3a0ae9d..08fb9b74f 100644 --- a/tests/test_dependency_cache.py +++ b/tests/test_dependency_cache.py @@ -29,7 +29,7 @@ async def get_sub_counter( @app.get("/sub-counter-no-cache/") async def get_sub_counter_no_cache( - subcount: int = Depends(dep_counter), + subcount: int = Depends(super_dep), count: int = Depends(dep_counter, use_cache=False), ): return {"counter": count, "subcounter": subcount}