mirror of
https://github.com/fastapi/fastapi.git
synced 2026-09-14 13:36:21 +08:00
⚡️ Reduce memory usage in dependencies (#16049)
This commit is contained in:
committed by
GitHub
parent
ae031be7b5
commit
0270829500
+174
-130
@@ -2,7 +2,7 @@ import inspect
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from functools import cached_property, partial
|
||||
from functools import lru_cache, partial
|
||||
from typing import Any, Literal
|
||||
|
||||
from fastapi._compat import ModelField
|
||||
@@ -28,7 +28,7 @@ def _impartial(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||
return func
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(slots=True)
|
||||
class Dependant:
|
||||
path_params: list[ModelField] = field(default_factory=list)
|
||||
query_params: list[ModelField] = field(default_factory=list)
|
||||
@@ -50,144 +50,188 @@ class Dependant:
|
||||
path: str | None = None
|
||||
scope: 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,
|
||||
scopes_for_cache,
|
||||
self.computed_scope or "",
|
||||
_UsesScopesCache = dict[int, tuple[Dependant, bool]]
|
||||
|
||||
|
||||
class _CallIdentity:
|
||||
__slots__ = ("call",)
|
||||
|
||||
def __init__(self, call: Callable[..., Any]) -> None:
|
||||
self.call = call
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return id(self.call)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, _CallIdentity) and self.call is other.call
|
||||
|
||||
|
||||
def _get_oauth_scopes(*, dependant: Dependant) -> list[str]:
|
||||
scopes = (
|
||||
dependant.parent_oauth_scopes.copy() if dependant.parent_oauth_scopes else []
|
||||
)
|
||||
# This doesn't use a set to preserve order, just in case
|
||||
for scope in dependant.own_oauth_scopes or []:
|
||||
if scope not in scopes:
|
||||
scopes.append(scope)
|
||||
return scopes
|
||||
|
||||
|
||||
def _get_cache_key(
|
||||
*,
|
||||
dependant: Dependant,
|
||||
uses_scopes_cache: _UsesScopesCache | None = None,
|
||||
) -> DependencyCacheKey:
|
||||
scopes_for_cache = (
|
||||
tuple(sorted(set(_get_oauth_scopes(dependant=dependant))))
|
||||
if _uses_scopes(dependant=dependant, cache=uses_scopes_cache)
|
||||
else ()
|
||||
)
|
||||
return (
|
||||
dependant.call,
|
||||
scopes_for_cache,
|
||||
_get_computed_scope(dependant=dependant) or "",
|
||||
)
|
||||
|
||||
|
||||
def _uses_scopes(
|
||||
*, dependant: Dependant, cache: _UsesScopesCache | None = None
|
||||
) -> bool:
|
||||
if cache is None:
|
||||
cache = {}
|
||||
cache_key = id(dependant)
|
||||
cached = cache.get(cache_key)
|
||||
if cached is not None and cached[0] is dependant:
|
||||
return cached[1]
|
||||
if dependant.own_oauth_scopes:
|
||||
result = True
|
||||
elif dependant.security_scopes_param_name is not None:
|
||||
result = True
|
||||
elif _is_security_scheme(dependant=dependant):
|
||||
result = True
|
||||
else:
|
||||
result = any(
|
||||
_uses_scopes(dependant=sub_dep, cache=cache)
|
||||
for sub_dep in dependant.dependencies
|
||||
)
|
||||
cache[cache_key] = (dependant, result)
|
||||
return result
|
||||
|
||||
@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
|
||||
if self._is_security_scheme:
|
||||
return True
|
||||
for sub_dep in self.dependencies:
|
||||
if sub_dep._uses_scopes:
|
||||
return True
|
||||
|
||||
def _is_security_scheme(*, dependant: Dependant) -> bool:
|
||||
if dependant.call is None:
|
||||
return False # pragma: no cover
|
||||
unwrapped = _unwrapped_call(dependant.call)
|
||||
return isinstance(unwrapped, SecurityBase)
|
||||
|
||||
|
||||
def _get_security_scheme(*, dependant: Dependant) -> SecurityBase:
|
||||
# Mainly to get the type of SecurityBase, but it's the same dependant.call
|
||||
unwrapped = _unwrapped_call(dependant.call)
|
||||
assert isinstance(unwrapped, SecurityBase)
|
||||
return unwrapped
|
||||
|
||||
|
||||
def _get_security_dependencies(*, dependant: Dependant) -> list[Dependant]:
|
||||
return [dep for dep in dependant.dependencies if _is_security_scheme(dependant=dep)]
|
||||
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def _is_gen_callable_cached(call_identity: _CallIdentity) -> bool:
|
||||
call = call_identity.call
|
||||
if inspect.isgeneratorfunction(_impartial(call)) or inspect.isgeneratorfunction(
|
||||
_unwrapped_call(call)
|
||||
):
|
||||
return True
|
||||
if inspect.isclass(_unwrapped_call(call)):
|
||||
return False
|
||||
dunder_call = getattr(_impartial(call), "__call__", None) # noqa: B004
|
||||
if dunder_call is None:
|
||||
return False # pragma: no cover
|
||||
if inspect.isgeneratorfunction(
|
||||
_impartial(dunder_call)
|
||||
) or inspect.isgeneratorfunction(_unwrapped_call(dunder_call)):
|
||||
return True
|
||||
dunder_unwrapped_call = getattr(_unwrapped_call(call), "__call__", None) # noqa: B004
|
||||
if dunder_unwrapped_call is None:
|
||||
return False # pragma: no cover
|
||||
return inspect.isgeneratorfunction(
|
||||
_impartial(dunder_unwrapped_call)
|
||||
) or inspect.isgeneratorfunction(_unwrapped_call(dunder_unwrapped_call))
|
||||
|
||||
@cached_property
|
||||
def _is_security_scheme(self) -> bool:
|
||||
if self.call is None:
|
||||
return False # pragma: no cover
|
||||
unwrapped = _unwrapped_call(self.call)
|
||||
return isinstance(unwrapped, SecurityBase)
|
||||
|
||||
# Mainly to get the type of SecurityBase, but it's the same self.call
|
||||
@cached_property
|
||||
def _security_scheme(self) -> SecurityBase:
|
||||
unwrapped = _unwrapped_call(self.call)
|
||||
assert isinstance(unwrapped, SecurityBase)
|
||||
return unwrapped
|
||||
def _is_gen_callable(call: Callable[..., Any] | None) -> bool:
|
||||
if call is None:
|
||||
return False # pragma: no cover
|
||||
return _is_gen_callable_cached(_CallIdentity(call))
|
||||
|
||||
@cached_property
|
||||
def _security_dependencies(self) -> list["Dependant"]:
|
||||
security_deps = [dep for dep in self.dependencies if dep._is_security_scheme]
|
||||
return security_deps
|
||||
|
||||
@cached_property
|
||||
def is_gen_callable(self) -> bool:
|
||||
if self.call is None:
|
||||
return False # pragma: no cover
|
||||
if inspect.isgeneratorfunction(
|
||||
_impartial(self.call)
|
||||
) or inspect.isgeneratorfunction(_unwrapped_call(self.call)):
|
||||
return True
|
||||
if inspect.isclass(_unwrapped_call(self.call)):
|
||||
return False
|
||||
dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004
|
||||
if dunder_call is None:
|
||||
return False # pragma: no cover
|
||||
if inspect.isgeneratorfunction(
|
||||
_impartial(dunder_call)
|
||||
) or inspect.isgeneratorfunction(_unwrapped_call(dunder_call)):
|
||||
return True
|
||||
dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004
|
||||
if dunder_unwrapped_call is None:
|
||||
return False # pragma: no cover
|
||||
if inspect.isgeneratorfunction(
|
||||
_impartial(dunder_unwrapped_call)
|
||||
) or inspect.isgeneratorfunction(_unwrapped_call(dunder_unwrapped_call)):
|
||||
return True
|
||||
@lru_cache(maxsize=1024)
|
||||
def _is_async_gen_callable_cached(call_identity: _CallIdentity) -> bool:
|
||||
call = call_identity.call
|
||||
if inspect.isasyncgenfunction(_impartial(call)) or inspect.isasyncgenfunction(
|
||||
_unwrapped_call(call)
|
||||
):
|
||||
return True
|
||||
if inspect.isclass(_unwrapped_call(call)):
|
||||
return False
|
||||
dunder_call = getattr(_impartial(call), "__call__", None) # noqa: B004
|
||||
if dunder_call is None:
|
||||
return False # pragma: no cover
|
||||
if inspect.isasyncgenfunction(
|
||||
_impartial(dunder_call)
|
||||
) or inspect.isasyncgenfunction(_unwrapped_call(dunder_call)):
|
||||
return True
|
||||
dunder_unwrapped_call = getattr(_unwrapped_call(call), "__call__", None) # noqa: B004
|
||||
if dunder_unwrapped_call is None:
|
||||
return False # pragma: no cover
|
||||
return inspect.isasyncgenfunction(
|
||||
_impartial(dunder_unwrapped_call)
|
||||
) or inspect.isasyncgenfunction(_unwrapped_call(dunder_unwrapped_call))
|
||||
|
||||
@cached_property
|
||||
def is_async_gen_callable(self) -> bool:
|
||||
if self.call is None:
|
||||
return False # pragma: no cover
|
||||
if inspect.isasyncgenfunction(
|
||||
_impartial(self.call)
|
||||
) or inspect.isasyncgenfunction(_unwrapped_call(self.call)):
|
||||
return True
|
||||
if inspect.isclass(_unwrapped_call(self.call)):
|
||||
return False
|
||||
dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004
|
||||
if dunder_call is None:
|
||||
return False # pragma: no cover
|
||||
if inspect.isasyncgenfunction(
|
||||
_impartial(dunder_call)
|
||||
) or inspect.isasyncgenfunction(_unwrapped_call(dunder_call)):
|
||||
return True
|
||||
dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004
|
||||
if dunder_unwrapped_call is None:
|
||||
return False # pragma: no cover
|
||||
if inspect.isasyncgenfunction(
|
||||
_impartial(dunder_unwrapped_call)
|
||||
) or inspect.isasyncgenfunction(_unwrapped_call(dunder_unwrapped_call)):
|
||||
return True
|
||||
|
||||
def _is_async_gen_callable(call: Callable[..., Any] | None) -> bool:
|
||||
if call is None:
|
||||
return False # pragma: no cover
|
||||
return _is_async_gen_callable_cached(_CallIdentity(call))
|
||||
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def _is_coroutine_callable_cached(call_identity: _CallIdentity) -> bool:
|
||||
call = call_identity.call
|
||||
if inspect.isroutine(_impartial(call)) and iscoroutinefunction(_impartial(call)):
|
||||
return True
|
||||
if inspect.isroutine(_unwrapped_call(call)) and iscoroutinefunction(
|
||||
_unwrapped_call(call)
|
||||
):
|
||||
return True
|
||||
if inspect.isclass(_unwrapped_call(call)):
|
||||
return False
|
||||
dunder_call = getattr(_impartial(call), "__call__", None) # noqa: B004
|
||||
if dunder_call is None:
|
||||
return False # pragma: no cover
|
||||
if iscoroutinefunction(_impartial(dunder_call)) or iscoroutinefunction(
|
||||
_unwrapped_call(dunder_call)
|
||||
):
|
||||
return True
|
||||
dunder_unwrapped_call = getattr(_unwrapped_call(call), "__call__", None) # noqa: B004
|
||||
if dunder_unwrapped_call is None:
|
||||
return False # pragma: no cover
|
||||
return iscoroutinefunction(
|
||||
_impartial(dunder_unwrapped_call)
|
||||
) or iscoroutinefunction(_unwrapped_call(dunder_unwrapped_call))
|
||||
|
||||
@cached_property
|
||||
def is_coroutine_callable(self) -> bool:
|
||||
if self.call is None:
|
||||
return False # pragma: no cover
|
||||
if inspect.isroutine(_impartial(self.call)) and iscoroutinefunction(
|
||||
_impartial(self.call)
|
||||
):
|
||||
return True
|
||||
if inspect.isroutine(_unwrapped_call(self.call)) and iscoroutinefunction(
|
||||
_unwrapped_call(self.call)
|
||||
):
|
||||
return True
|
||||
if inspect.isclass(_unwrapped_call(self.call)):
|
||||
return False
|
||||
dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004
|
||||
if dunder_call is None:
|
||||
return False # pragma: no cover
|
||||
if iscoroutinefunction(_impartial(dunder_call)) or iscoroutinefunction(
|
||||
_unwrapped_call(dunder_call)
|
||||
):
|
||||
return True
|
||||
dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004
|
||||
if dunder_unwrapped_call is None:
|
||||
return False # pragma: no cover
|
||||
if iscoroutinefunction(
|
||||
_impartial(dunder_unwrapped_call)
|
||||
) or iscoroutinefunction(_unwrapped_call(dunder_unwrapped_call)):
|
||||
return True
|
||||
return False
|
||||
|
||||
@cached_property
|
||||
def computed_scope(self) -> str | None:
|
||||
if self.scope:
|
||||
return self.scope
|
||||
if self.is_gen_callable or self.is_async_gen_callable:
|
||||
return "request"
|
||||
return None
|
||||
def _is_coroutine_callable(call: Callable[..., Any] | None) -> bool:
|
||||
if call is None:
|
||||
return False # pragma: no cover
|
||||
return _is_coroutine_callable_cached(_CallIdentity(call))
|
||||
|
||||
|
||||
def _get_computed_scope(*, dependant: Dependant) -> str | None:
|
||||
if dependant.scope:
|
||||
return dependant.scope
|
||||
if _is_gen_callable(dependant.call) or _is_async_gen_callable(dependant.call):
|
||||
return "request"
|
||||
return None
|
||||
|
||||
@@ -54,7 +54,16 @@ from fastapi.concurrency import (
|
||||
asynccontextmanager,
|
||||
contextmanager_in_threadpool,
|
||||
)
|
||||
from fastapi.dependencies.models import Dependant
|
||||
from fastapi.dependencies.models import (
|
||||
Dependant,
|
||||
_get_cache_key,
|
||||
_get_computed_scope,
|
||||
_get_oauth_scopes,
|
||||
_is_async_gen_callable,
|
||||
_is_coroutine_callable,
|
||||
_is_gen_callable,
|
||||
_UsesScopesCache,
|
||||
)
|
||||
from fastapi.exceptions import DependencyScopeError
|
||||
from fastapi.logger import logger
|
||||
from fastapi.security.oauth2 import SecurityScopes
|
||||
@@ -141,12 +150,20 @@ def get_flat_dependant(
|
||||
skip_repeats: bool = False,
|
||||
visited: list[DependencyCacheKey] | None = None,
|
||||
parent_oauth_scopes: list[str] | None = None,
|
||||
_uses_scopes_cache: _UsesScopesCache | None = None,
|
||||
) -> Dependant:
|
||||
if visited is None:
|
||||
visited = []
|
||||
visited.append(dependant.cache_key)
|
||||
if _uses_scopes_cache is None:
|
||||
_uses_scopes_cache = {}
|
||||
visited.append(
|
||||
_get_cache_key(
|
||||
dependant=dependant,
|
||||
uses_scopes_cache=_uses_scopes_cache,
|
||||
)
|
||||
)
|
||||
use_parent_oauth_scopes = (parent_oauth_scopes or []) + (
|
||||
dependant.oauth_scopes or []
|
||||
_get_oauth_scopes(dependant=dependant)
|
||||
)
|
||||
|
||||
flat_dependant = Dependant(
|
||||
@@ -170,13 +187,21 @@ def get_flat_dependant(
|
||||
scope=dependant.scope,
|
||||
)
|
||||
for sub_dependant in dependant.dependencies:
|
||||
if skip_repeats and sub_dependant.cache_key in visited:
|
||||
if (
|
||||
skip_repeats
|
||||
and _get_cache_key(
|
||||
dependant=sub_dependant,
|
||||
uses_scopes_cache=_uses_scopes_cache,
|
||||
)
|
||||
in visited
|
||||
):
|
||||
continue
|
||||
flat_sub = get_flat_dependant(
|
||||
sub_dependant,
|
||||
skip_repeats=skip_repeats,
|
||||
visited=visited,
|
||||
parent_oauth_scopes=flat_dependant.oauth_scopes,
|
||||
parent_oauth_scopes=_get_oauth_scopes(dependant=flat_dependant),
|
||||
_uses_scopes_cache=_uses_scopes_cache,
|
||||
)
|
||||
flat_dependant.dependencies.append(flat_sub)
|
||||
flat_dependant.path_params.extend(flat_sub.path_params)
|
||||
@@ -317,8 +342,11 @@ def get_dependant(
|
||||
if param_details.depends is not None:
|
||||
assert param_details.depends.dependency
|
||||
if (
|
||||
(dependant.is_gen_callable or dependant.is_async_gen_callable)
|
||||
and dependant.computed_scope == "request"
|
||||
(
|
||||
_is_gen_callable(dependant.call)
|
||||
or _is_async_gen_callable(dependant.call)
|
||||
)
|
||||
and _get_computed_scope(dependant=dependant) == "request"
|
||||
and param_details.depends.scope == "function"
|
||||
):
|
||||
assert dependant.call
|
||||
@@ -579,9 +607,9 @@ async def _solve_generator(
|
||||
*, dependant: Dependant, stack: AsyncExitStack, sub_values: dict[str, Any]
|
||||
) -> Any:
|
||||
assert dependant.call
|
||||
if dependant.is_async_gen_callable:
|
||||
if _is_async_gen_callable(dependant.call):
|
||||
cm = asynccontextmanager(dependant.call)(**sub_values)
|
||||
elif dependant.is_gen_callable:
|
||||
elif _is_gen_callable(dependant.call):
|
||||
cm = contextmanager_in_threadpool(contextmanager(dependant.call)(**sub_values))
|
||||
return await stack.enter_async_context(cm)
|
||||
|
||||
@@ -608,6 +636,7 @@ async def solve_dependencies(
|
||||
# people might be monkey patching this function (although that's not supported)
|
||||
async_exit_stack: AsyncExitStack,
|
||||
embed_body_fields: bool,
|
||||
_uses_scopes_cache: _UsesScopesCache | None = None,
|
||||
) -> SolvedDependency:
|
||||
request_astack = request.scope.get("fastapi_inner_astack")
|
||||
assert isinstance(request_astack, AsyncExitStack), (
|
||||
@@ -625,6 +654,8 @@ async def solve_dependencies(
|
||||
response.status_code = None # type: ignore
|
||||
if dependency_cache is None:
|
||||
dependency_cache = {}
|
||||
if _uses_scopes_cache is None:
|
||||
_uses_scopes_cache = {}
|
||||
for sub_dependant in dependant.dependencies:
|
||||
sub_dependant.call = cast(Callable[..., Any], sub_dependant.call)
|
||||
call = sub_dependant.call
|
||||
@@ -642,7 +673,7 @@ async def solve_dependencies(
|
||||
path=use_path,
|
||||
call=call,
|
||||
name=sub_dependant.name,
|
||||
parent_oauth_scopes=sub_dependant.oauth_scopes,
|
||||
parent_oauth_scopes=_get_oauth_scopes(dependant=sub_dependant),
|
||||
scope=sub_dependant.scope,
|
||||
)
|
||||
|
||||
@@ -656,15 +687,20 @@ async def solve_dependencies(
|
||||
dependency_cache=dependency_cache,
|
||||
async_exit_stack=async_exit_stack,
|
||||
embed_body_fields=embed_body_fields,
|
||||
_uses_scopes_cache=_uses_scopes_cache,
|
||||
)
|
||||
background_tasks = solved_result.background_tasks
|
||||
if solved_result.errors:
|
||||
errors.extend(solved_result.errors)
|
||||
continue
|
||||
if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache:
|
||||
solved = dependency_cache[sub_dependant.cache_key]
|
||||
elif (
|
||||
use_sub_dependant.is_gen_callable or use_sub_dependant.is_async_gen_callable
|
||||
sub_dependant_cache_key = _get_cache_key(
|
||||
dependant=sub_dependant,
|
||||
uses_scopes_cache=_uses_scopes_cache,
|
||||
)
|
||||
if sub_dependant.use_cache and sub_dependant_cache_key in dependency_cache:
|
||||
solved = dependency_cache[sub_dependant_cache_key]
|
||||
elif _is_gen_callable(use_sub_dependant.call) or _is_async_gen_callable(
|
||||
use_sub_dependant.call
|
||||
):
|
||||
use_astack = request_astack
|
||||
if sub_dependant.scope == "function":
|
||||
@@ -674,14 +710,14 @@ async def solve_dependencies(
|
||||
stack=use_astack,
|
||||
sub_values=solved_result.values,
|
||||
)
|
||||
elif use_sub_dependant.is_coroutine_callable:
|
||||
elif _is_coroutine_callable(use_sub_dependant.call):
|
||||
solved = await call(**solved_result.values)
|
||||
else:
|
||||
solved = await run_in_threadpool(call, **solved_result.values)
|
||||
if sub_dependant.name is not None:
|
||||
values[sub_dependant.name] = solved
|
||||
if sub_dependant.cache_key not in dependency_cache:
|
||||
dependency_cache[sub_dependant.cache_key] = solved
|
||||
if sub_dependant_cache_key not in dependency_cache:
|
||||
dependency_cache[sub_dependant_cache_key] = solved
|
||||
path_values, path_errors = request_params_to_args(
|
||||
dependant.path_params, request.path_params
|
||||
)
|
||||
@@ -724,7 +760,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.oauth_scopes
|
||||
scopes=_get_oauth_scopes(dependant=dependant)
|
||||
)
|
||||
return SolvedDependency(
|
||||
values=values,
|
||||
|
||||
@@ -15,7 +15,12 @@ from fastapi._compat import (
|
||||
lenient_issubclass,
|
||||
)
|
||||
from fastapi.datastructures import DefaultPlaceholder, _Unset
|
||||
from fastapi.dependencies.models import Dependant
|
||||
from fastapi.dependencies.models import (
|
||||
Dependant,
|
||||
_get_oauth_scopes,
|
||||
_get_security_dependencies,
|
||||
_get_security_scheme,
|
||||
)
|
||||
from fastapi.dependencies.utils import (
|
||||
_get_flat_fields_from_params,
|
||||
get_flat_dependant,
|
||||
@@ -84,18 +89,19 @@ def get_openapi_security_definitions(
|
||||
security_definitions = {}
|
||||
# Use a dict to merge scopes for same security scheme
|
||||
operation_security_dict: dict[str, list[str]] = {}
|
||||
for security_dependency in flat_dependant._security_dependencies:
|
||||
for security_dependency in _get_security_dependencies(dependant=flat_dependant):
|
||||
security_scheme = _get_security_scheme(dependant=security_dependency)
|
||||
security_definition = jsonable_encoder(
|
||||
security_dependency._security_scheme.model,
|
||||
security_scheme.model,
|
||||
by_alias=True,
|
||||
exclude_none=True,
|
||||
)
|
||||
security_name = security_dependency._security_scheme.scheme_name
|
||||
security_name = security_scheme.scheme_name
|
||||
security_definitions[security_name] = security_definition
|
||||
# Merge scopes for the same security scheme
|
||||
if security_name not in operation_security_dict:
|
||||
operation_security_dict[security_name] = []
|
||||
for scope in security_dependency.oauth_scopes or []:
|
||||
for scope in _get_oauth_scopes(dependant=security_dependency):
|
||||
if scope not in operation_security_dict[security_name]:
|
||||
operation_security_dict[security_name].append(scope)
|
||||
operation_security = [
|
||||
|
||||
+15
-8
@@ -48,7 +48,12 @@ from fastapi._compat import (
|
||||
lenient_issubclass,
|
||||
)
|
||||
from fastapi.datastructures import Default, DefaultPlaceholder
|
||||
from fastapi.dependencies.models import Dependant
|
||||
from fastapi.dependencies.models import (
|
||||
Dependant,
|
||||
_is_async_gen_callable,
|
||||
_is_coroutine_callable,
|
||||
_is_gen_callable,
|
||||
)
|
||||
from fastapi.dependencies.utils import (
|
||||
_should_embed_body_fields,
|
||||
get_body_field,
|
||||
@@ -384,7 +389,7 @@ def get_request_handler(
|
||||
is_json_stream: bool = False,
|
||||
) -> Callable[[Request], Coroutine[Any, Any, Response]]:
|
||||
assert dependant.call is not None, "dependant.call must be a function"
|
||||
is_coroutine = dependant.is_coroutine_callable
|
||||
is_coroutine = _is_coroutine_callable(dependant.call)
|
||||
is_body_form = body_field and isinstance(body_field.field_info, params.Form)
|
||||
if isinstance(response_class, DefaultPlaceholder):
|
||||
actual_response_class: type[Response] = response_class.value
|
||||
@@ -543,7 +548,7 @@ def get_request_handler(
|
||||
data_str=_serialize_data(item).decode("utf-8")
|
||||
)
|
||||
|
||||
if dependant.is_async_gen_callable:
|
||||
if _is_async_gen_callable(dependant.call):
|
||||
sse_aiter: AsyncIterator[Any] = gen.__aiter__()
|
||||
else:
|
||||
sse_aiter = iterate_in_threadpool(gen)
|
||||
@@ -641,7 +646,7 @@ def get_request_handler(
|
||||
def _serialize_item(item: Any) -> bytes:
|
||||
return _serialize_data(item) + b"\n"
|
||||
|
||||
if dependant.is_async_gen_callable:
|
||||
if _is_async_gen_callable(dependant.call):
|
||||
|
||||
async def _async_stream_jsonl() -> AsyncIterator[bytes]:
|
||||
async for item in gen:
|
||||
@@ -667,10 +672,12 @@ def get_request_handler(
|
||||
background=solved_result.background_tasks,
|
||||
)
|
||||
response.headers.raw.extend(solved_result.response.headers.raw)
|
||||
elif dependant.is_async_gen_callable or dependant.is_gen_callable:
|
||||
elif _is_async_gen_callable(dependant.call) or _is_gen_callable(
|
||||
dependant.call
|
||||
):
|
||||
# Raw streaming with explicit response_class (e.g. StreamingResponse)
|
||||
gen = dependant.call(**solved_result.values)
|
||||
if dependant.is_async_gen_callable:
|
||||
if _is_async_gen_callable(dependant.call):
|
||||
|
||||
async def _async_stream_raw(
|
||||
async_gen: AsyncIterator[Any],
|
||||
@@ -1097,8 +1104,8 @@ def _populate_api_route_state(
|
||||
embed_body_fields=route._embed_body_fields,
|
||||
)
|
||||
# Detect generator endpoints that should stream as JSONL or SSE
|
||||
is_generator = (
|
||||
route.dependant.is_async_gen_callable or route.dependant.is_gen_callable
|
||||
is_generator = _is_async_gen_callable(route.dependant.call) or _is_gen_callable(
|
||||
route.dependant.call
|
||||
)
|
||||
route.is_sse_stream = is_generator and lenient_issubclass(
|
||||
response_class, EventSourceResponse
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
from collections.abc import AsyncGenerator, Generator
|
||||
from typing import Any
|
||||
|
||||
from fastapi.dependencies.models import (
|
||||
Dependant,
|
||||
_get_cache_key,
|
||||
_get_computed_scope,
|
||||
_get_oauth_scopes,
|
||||
_get_security_dependencies,
|
||||
_get_security_scheme,
|
||||
_is_async_gen_callable,
|
||||
_is_async_gen_callable_cached,
|
||||
_is_coroutine_callable,
|
||||
_is_coroutine_callable_cached,
|
||||
_is_gen_callable,
|
||||
_is_gen_callable_cached,
|
||||
_is_security_scheme,
|
||||
_uses_scopes,
|
||||
)
|
||||
from fastapi.security import APIKeyHeader
|
||||
|
||||
|
||||
def sync_dependency() -> None:
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
async def async_dependency() -> None:
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
def generator_dependency() -> Generator[None, None, None]:
|
||||
yield # pragma: no cover
|
||||
|
||||
|
||||
async def async_generator_dependency() -> AsyncGenerator[None, None]:
|
||||
yield # pragma: no cover
|
||||
|
||||
|
||||
class UnhashableCallable:
|
||||
__hash__ = None
|
||||
|
||||
async def __call__(self) -> None:
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
class UnhashableGeneratorCallable:
|
||||
__hash__ = None
|
||||
|
||||
def __call__(self) -> Generator[None, None, None]:
|
||||
yield # pragma: no cover
|
||||
|
||||
|
||||
class UnhashableAsyncGeneratorCallable:
|
||||
__hash__ = None
|
||||
|
||||
async def __call__(self) -> AsyncGenerator[None, None]:
|
||||
yield # pragma: no cover
|
||||
|
||||
|
||||
class EqualCallable:
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, EqualCallable)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return 1 # pragma: no cover
|
||||
|
||||
|
||||
class EqualAsyncCallable(EqualCallable):
|
||||
async def __call__(self) -> None:
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
class EqualSyncCallable(EqualCallable):
|
||||
def __call__(self) -> None:
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
def test_callable_classification_is_shared_by_call() -> None:
|
||||
_is_gen_callable_cached.cache_clear()
|
||||
_is_async_gen_callable_cached.cache_clear()
|
||||
_is_coroutine_callable_cached.cache_clear()
|
||||
|
||||
for _ in range(2):
|
||||
assert not _is_gen_callable(async_dependency)
|
||||
assert not _is_async_gen_callable(async_dependency)
|
||||
assert _is_coroutine_callable(async_dependency)
|
||||
|
||||
for cached_function in (
|
||||
_is_gen_callable_cached,
|
||||
_is_async_gen_callable_cached,
|
||||
_is_coroutine_callable_cached,
|
||||
):
|
||||
cache_info = cached_function.cache_info()
|
||||
assert cache_info.hits == 1
|
||||
assert cache_info.misses == 1
|
||||
assert cache_info.maxsize == 1024
|
||||
|
||||
|
||||
def test_unhashable_callable_classification() -> None:
|
||||
assert _is_coroutine_callable(UnhashableCallable())
|
||||
assert _is_gen_callable(UnhashableGeneratorCallable())
|
||||
assert _is_async_gen_callable(UnhashableAsyncGeneratorCallable())
|
||||
|
||||
|
||||
def test_equal_callable_instances_are_cached_by_identity() -> None:
|
||||
async_callable = EqualAsyncCallable()
|
||||
sync_callable = EqualSyncCallable()
|
||||
|
||||
assert async_callable == sync_callable
|
||||
assert _is_coroutine_callable(async_callable)
|
||||
assert not _is_coroutine_callable(sync_callable)
|
||||
|
||||
|
||||
def test_callable_classification() -> None:
|
||||
assert not _is_gen_callable(sync_dependency)
|
||||
assert not _is_async_gen_callable(sync_dependency)
|
||||
assert not _is_coroutine_callable(sync_dependency)
|
||||
assert _is_gen_callable(generator_dependency)
|
||||
assert _is_async_gen_callable(async_generator_dependency)
|
||||
|
||||
|
||||
def test_derived_values_are_not_stored_on_dependant() -> None:
|
||||
dependant = Dependant(call=async_dependency)
|
||||
uses_scopes_cache = {}
|
||||
|
||||
assert _get_oauth_scopes(dependant=dependant) == []
|
||||
assert not _uses_scopes(dependant=dependant, cache=uses_scopes_cache)
|
||||
assert not _uses_scopes(dependant=dependant, cache=uses_scopes_cache)
|
||||
assert _get_security_dependencies(dependant=dependant) == []
|
||||
assert _get_computed_scope(dependant=dependant) is None
|
||||
assert _get_cache_key(dependant=dependant) == (async_dependency, (), "")
|
||||
|
||||
assert not hasattr(dependant, "__dict__")
|
||||
|
||||
|
||||
def test_security_scheme_helpers() -> None:
|
||||
security_scheme = APIKeyHeader(name="key")
|
||||
security_dependant = Dependant(call=security_scheme)
|
||||
dependant = Dependant(dependencies=[security_dependant])
|
||||
|
||||
assert _is_security_scheme(dependant=security_dependant)
|
||||
assert _get_security_scheme(dependant=security_dependant) is security_scheme
|
||||
assert _get_security_dependencies(dependant=dependant) == [security_dependant]
|
||||
assert _uses_scopes(dependant=dependant)
|
||||
|
||||
|
||||
def test_derived_values_follow_dependency_state() -> None:
|
||||
child = Dependant(call=sync_dependency)
|
||||
dependant = Dependant(
|
||||
call=sync_dependency,
|
||||
dependencies=[child],
|
||||
own_oauth_scopes=[],
|
||||
parent_oauth_scopes=["parent"],
|
||||
)
|
||||
|
||||
assert _get_cache_key(dependant=dependant) == (sync_dependency, (), "")
|
||||
|
||||
child.security_scopes_param_name = "scopes"
|
||||
dependant.own_oauth_scopes = ["own", "parent"]
|
||||
|
||||
assert _uses_scopes(dependant=dependant)
|
||||
assert _get_oauth_scopes(dependant=dependant) == ["parent", "own"]
|
||||
assert _get_cache_key(dependant=dependant) == (
|
||||
sync_dependency,
|
||||
("own", "parent"),
|
||||
"",
|
||||
)
|
||||
|
||||
|
||||
def test_explicit_and_generator_scopes() -> None:
|
||||
assert (
|
||||
_get_computed_scope(dependant=Dependant(call=sync_dependency, scope="function"))
|
||||
== "function"
|
||||
)
|
||||
assert (
|
||||
_get_computed_scope(dependant=Dependant(call=generator_dependency)) == "request"
|
||||
)
|
||||
|
||||
|
||||
def test_callable_return_annotations_are_not_used() -> None:
|
||||
class CallableWithUnhashableReturn:
|
||||
def __call__(self) -> Any:
|
||||
return None # pragma: no cover
|
||||
|
||||
__hash__ = None
|
||||
|
||||
assert not _is_coroutine_callable(CallableWithUnhashableReturn())
|
||||
Reference in New Issue
Block a user