From 8381fcf425a795be334be56cd393007f61640de9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 30 Nov 2025 15:26:04 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Cache=20dependencies=20that=20do?= =?UTF-8?q?n't=20use=20scopes=20and=20don't=20have=20sub-dependencies=20wi?= =?UTF-8?q?th=20scopes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/models.py | 30 ++++++++++++++++++++++++++++-- fastapi/dependencies/utils.py | 31 +++++++++++++++---------------- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index d6359c0f5..7ea136c64 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -38,19 +38,45 @@ class Dependant: response_param_name: Optional[str] = None background_tasks_param_name: Optional[str] = None security_scopes_param_name: Optional[str] = None - security_scopes: Optional[List[str]] = None + own_oauth_scopes: Optional[List[str]] = None + parent_oauth_scopes: Optional[List[str]] = None use_cache: bool = True path: Optional[str] = None scope: Union[Literal["function", "request"], None] = None + @cached_property + def oauth_scopes(self) -> List[str]: + scopes = self.parent_oauth_scopes.copy() if self.parent_oauth_scopes else [] + # This doesn't use a set to preserve order, just in case + for scope in self.own_oauth_scopes or []: + if scope not in scopes: + scopes.append(scope) + return scopes + @cached_property def cache_key(self) -> DependencyCacheKey: + scopes_for_cache = ( + tuple(sorted(set(self.oauth_scopes or []))) + if self._uses_scopes + else () + ) return ( self.call, - tuple(sorted(set(self.security_scopes or []))), + scopes_for_cache, self.computed_scope or "", ) + @cached_property + def _uses_scopes(self) -> bool: + if self.own_oauth_scopes: + return True + if self.security_scopes_param_name is not None: + return True + for sub_dep in self.dependencies: + if sub_dep._uses_scopes: + return True + return False + @cached_property def is_gen_callable(self) -> bool: if inspect.isgeneratorfunction(self.call): diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 45353835b..a982c3b52 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -126,14 +126,14 @@ def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> De assert callable(depends.dependency), ( "A parameter-less dependency must have a callable dependency" ) - use_security_scopes: List[str] = [] + own_oauth_scopes: List[str] = [] if isinstance(depends, params.Security) and depends.scopes: - use_security_scopes.extend(depends.scopes) + own_oauth_scopes.extend(depends.scopes) return get_dependant( path=path, call=depends.dependency, scope=depends.scope, - security_scopes=use_security_scopes, + own_oauth_scopes=own_oauth_scopes, ) @@ -232,7 +232,8 @@ def get_dependant( path: str, call: Callable[..., Any], name: Optional[str] = None, - security_scopes: Optional[List[str]] = None, + own_oauth_scopes: Optional[List[str]] = None, + parent_oauth_scopes: Optional[List[str]] = None, use_cache: bool = True, scope: Union[Literal["function", "request"], None] = None, ) -> Dependant: @@ -240,19 +241,18 @@ def get_dependant( call=call, name=name, path=path, - security_scopes=security_scopes, use_cache=use_cache, scope=scope, + own_oauth_scopes=own_oauth_scopes, + parent_oauth_scopes=parent_oauth_scopes, ) + current_scopes = (parent_oauth_scopes or []) + (own_oauth_scopes or []) path_param_names = get_path_param_names(path) endpoint_signature = get_typed_signature(call) signature_params = endpoint_signature.parameters if isinstance(call, SecurityBase): - use_scopes: List[str] = [] - if isinstance(call, (OAuth2, OpenIdConnect)): - use_scopes = security_scopes or use_scopes security_requirement = SecurityRequirement( - security_scheme=call, scopes=use_scopes + security_scheme=call, scopes=current_scopes ) dependant.security_requirements.append(security_requirement) for param_name, param in signature_params.items(): @@ -275,17 +275,16 @@ def get_dependant( f'The dependency "{dependant.call.__name__}" has a scope of ' '"request", it cannot depend on dependencies with scope "function".' ) - use_security_scopes = security_scopes or [] + sub_own_oauth_scopes: List[str] = [] if isinstance(param_details.depends, params.Security): if param_details.depends.scopes: - use_security_scopes = use_security_scopes + list( - param_details.depends.scopes - ) + sub_own_oauth_scopes = list(param_details.depends.scopes) sub_dependant = get_dependant( path=path, call=param_details.depends.dependency, name=param_name, - security_scopes=use_security_scopes, + own_oauth_scopes=sub_own_oauth_scopes, + parent_oauth_scopes=current_scopes, use_cache=param_details.depends.use_cache, scope=param_details.depends.scope, ) @@ -611,7 +610,7 @@ async def solve_dependencies( path=use_path, call=call, name=sub_dependant.name, - security_scopes=sub_dependant.security_scopes, + parent_oauth_scopes=sub_dependant.oauth_scopes, scope=sub_dependant.scope, ) @@ -693,7 +692,7 @@ async def solve_dependencies( values[dependant.response_param_name] = response if dependant.security_scopes_param_name: values[dependant.security_scopes_param_name] = SecurityScopes( - scopes=dependant.security_scopes + scopes=dependant.oauth_scopes ) return SolvedDependency( values=values,