resolved RESOLVED UNSUBMITTED PASS ยท None tool calls ยท 0 s ยท fastapi/fastapi
๐ Cache dependencies that don't use scopes and don't have sub-dependencies with scopes ๐ Cache dependencies that don't use scopes and don't have sub-dependencies with scopes Related to https://github.com/fastapi/fastapi/pull/9790, and https://github.com/fastapi/fastapi/discussions/6024 Uses the ideas in @YuriiMotov's diagrams here for the tests: https://github.com/fastapi/fastapi/discussions/6024#discussioncomment-8541913 --- Dependencies are cached based on the function. And also on the **scopes**, but (now) **only** when those scopes are used by this dependency or by a sub-dependency: this dependency or a sub-dependency declare scopes or access the `SecurityScopes`.
| # | Tool | Arguments | Result |
|---|---|---|---|
| No trace captured. | |||
--- a/fastapi/dependencies/models.py
+++ b/fastapi/dependencies/models.py
@@ -38,19 +38,43 @@ 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):
--- a/fastapi/dependencies/utils.py
+++ b/fastapi/dependencies/utils.py
@@ -58,8 +58,7 @@
from fastapi.exceptions import DependencyScopeError
from fastapi.logger import logger
from fastapi.security.base import SecurityBase
-from fastapi.security.oauth2 import OAuth2, SecurityScopes
-from fastapi.security.open_id_connect_url import OpenIdConnect
+from fastapi.security.oauth2 import SecurityScopes
from fastapi.types import DependencyCacheKey
from fastapi.utils import create_model_field, get_path_param_names
from pydantic import BaseModel
@@ -126,14 +125,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,27 +231,27 @@ 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:
dependant = 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 +274,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 +609,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 +691,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,.. [100%]
=============================== warnings summary ===============================
../../../../../../../Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45
/Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45: DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead.
_PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]]
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
2 passed, 1 warning in 0.76s