diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index 22df740c9..4dde37aa5 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -6,8 +6,14 @@ from typing import Any, Callable, List, Optional, Sequence, Union, Tuple, cast from fastapi._compat import ModelField from fastapi.security.base import SecurityBase -from fastapi.types import EndpointDependencyCacheKey, LifespanDependencyCacheKey -from typing_extensions import Literal, TypeAlias +from fastapi.types import ( + EndpointDependencyCacheKey, + LifespanDependencyCacheKey, + DependencyScope, + LifespanDependencyScope, + EndpointDependencyScope +) +from typing_extensions import TypeAlias if sys.version_info >= (3, 13): # pragma: no cover from inspect import iscoroutinefunction @@ -21,14 +27,13 @@ class SecurityRequirement: scopes: Optional[Sequence[str]] = None -@dataclass -class LifespanDependant: - call: Callable[..., Any] - caller: Callable[..., Any] - dependencies: List["LifespanDependant"] = field(default_factory=list) +@dataclass(kw_only=True, slots=True) +class _BaseDependant: name: Optional[str] = None - use_cache: bool = True index: Optional[int] = None + call: Optional[Callable[..., Any]] = None + scope: DependencyScope = None + use_cache: bool = True @cached_property def is_gen_callable(self) -> bool: @@ -53,6 +58,14 @@ class LifespanDependant: dunder_call = getattr(self.call, "__call__", None) # noqa: B004 return iscoroutinefunction(dunder_call) + +@dataclass +class LifespanDependant(_BaseDependant): + scope: LifespanDependencyScope + caller: Callable[..., Any] + call: Callable[..., Any] + dependencies: List["LifespanDependant"] = field(default_factory=list) + @cached_property def cache_key(self) -> LifespanDependencyCacheKey: if self.use_cache: @@ -66,7 +79,8 @@ class LifespanDependant: return self.caller, self.index @dataclass -class EndpointDependant: +class EndpointDependant(_BaseDependant): + scope: EndpointDependencyScope = None path_params: List[ModelField] = field(default_factory=list) query_params: List[ModelField] = field(default_factory=list) header_params: List[ModelField] = field(default_factory=list) @@ -75,9 +89,6 @@ class EndpointDependant: endpoint_dependencies: List["EndpointDependant"] = field(default_factory=list) lifespan_dependencies: List[LifespanDependant] = field(default_factory=list) security_requirements: List[SecurityRequirement] = field(default_factory=list) - name: Optional[str] = None - call: Optional[Callable[..., Any]] = None - index: Optional[int] = None request_param_name: Optional[str] = None websocket_param_name: Optional[str] = None http_connection_param_name: Optional[str] = None @@ -85,9 +96,7 @@ class EndpointDependant: background_tasks_param_name: Optional[str] = None security_scopes_param_name: Optional[str] = None security_scopes: Optional[List[str]] = None - use_cache: bool = True path: Optional[str] = None - scope: Union[Literal["function", "request"], None] = None @cached_property def cache_key(self) -> EndpointDependencyCacheKey: @@ -97,29 +106,6 @@ class EndpointDependant: self.computed_scope or "", ) - @cached_property - def is_gen_callable(self) -> bool: - if inspect.isgeneratorfunction(self.call): - return True - dunder_call = getattr(self.call, "__call__", None) # noqa: B004 - return inspect.isgeneratorfunction(dunder_call) - - @cached_property - def is_async_gen_callable(self) -> bool: - if inspect.isasyncgenfunction(self.call): - return True - dunder_call = getattr(self.call, "__call__", None) # noqa: B004 - return inspect.isasyncgenfunction(dunder_call) - - @cached_property - def is_coroutine_callable(self) -> bool: - if inspect.isroutine(self.call): - return iscoroutinefunction(self.call) - if inspect.isclass(self.call): - return False - dunder_call = getattr(self.call, "__call__", None) # noqa: B004 - return iscoroutinefunction(dunder_call) - @cached_property def computed_scope(self) -> Union[str, None]: if self.scope: diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index a58f24f56..6a3ee370d 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -59,7 +59,7 @@ 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.types import EndpointDependencyCacheKey, LifespanDependencyCacheKey +from fastapi.types import EndpointDependencyCacheKey, LifespanDependencyCacheKey, LifespanDependencyScope from fastapi.utils import create_model_field, get_path_param_names from pydantic import BaseModel from pydantic.fields import FieldInfo @@ -143,6 +143,7 @@ def get_parameterless_sub_dependant( call=depends.dependency, use_cache=depends.use_cache, index=index, + scope=depends.scope, ) else: raise InvalidDependencyScope( @@ -247,6 +248,7 @@ def get_lifespan_dependant( *, caller: Callable[..., Any], call: Callable[..., Any], + scope: LifespanDependencyScope, name: Optional[str] = None, use_cache: bool = True, index: Optional[int] = None, @@ -254,7 +256,7 @@ def get_lifespan_dependant( dependency_signature = get_typed_signature(call) signature_params = dependency_signature.parameters dependant = LifespanDependant( - call=call, name=name, use_cache=use_cache, caller=caller, index=index + call=call, name=name, use_cache=use_cache, caller=caller, index=index, scope=scope ) for param_name, param in signature_params.items(): param_details = analyze_param( @@ -265,7 +267,7 @@ def get_lifespan_dependant( ) if param_details.depends is None: raise DependencyScopeError( - f'Dependency "{dependant.name}" has "lifespan" scope, but was defined' + f'Dependency "{dependant.name}" has "{scope}" scope, but was defined' f'with an invalid argument: "{param_name}" which is ' f'not a valid sub-dependency. Lifespan scoped dependencies may only ' f"use lifespan scoped sub-dependencies." @@ -273,7 +275,7 @@ def get_lifespan_dependant( if param_details.depends.scope != "lifespan": raise DependencyScopeError( - f'Dependency "{dependant.name}" has "lifespan" scope, but was defined with the ' + f'Dependency "{dependant.name}" has "{scope}" scope, but was defined with the ' f'sub-dependency "{param_name}" which has "{param_details.depends.scope}" scope. Lifespan scoped ' f'dependencies may only use lifespan scoped sub-dependencies.' ) @@ -285,6 +287,7 @@ def get_lifespan_dependant( call=param_details.depends.dependency, use_cache=param_details.depends.use_cache, caller=call, + scope=scope ) dependant.dependencies.append(sub_dependant) @@ -351,7 +354,8 @@ def get_endpoint_dependant( call=param_details.depends.dependency, name=param_name, use_cache=param_details.depends.use_cache, - index=index + index=index, + scope=param_details.depends.scope )) elif param_details.depends.scope in ("request", "function", None): dependant.endpoint_dependencies.append(get_endpoint_dependant( @@ -672,6 +676,7 @@ async def solve_lifespan_dependant( name=dependant.name, use_cache=dependant.use_cache, index=dependant.index, + scope=dependant.scope ) dependency_arguments: Dict[str, Any] = {} diff --git a/fastapi/params.py b/fastapi/params.py index 96bd67024..ae990c856 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -13,10 +13,9 @@ from ._compat import ( Undefined, ) from .exceptions import InvalidDependencyScope +from .types import EndpointDependencyScope, DependencyScope _Unset: Any = Undefined -_EndpointDependencyScope: TypeAlias = Literal["request", "function"] -DependencyScope: TypeAlias = Union[Literal["lifespan"], _EndpointDependencyScope] class ParamTypes(Enum): @@ -769,7 +768,7 @@ class File(Form): # type: ignore[misc] class Depends: dependency: Optional[Callable[..., Any]] = None use_cache: bool = True - scope: Union[DependencyScope, None] = None + scope: DependencyScope = None def __post_init__(self): if self.scope not in ("lifespan", "request", "function", None): @@ -778,5 +777,5 @@ class Depends: @dataclass class Security(Depends): - scope: Union[_EndpointDependencyScope, None] = None + scope: EndpointDependencyScope = None scopes: Optional[Sequence[str]] = None diff --git a/fastapi/types.py b/fastapi/types.py index 562f849a5..2912f08ba 100644 --- a/fastapi/types.py +++ b/fastapi/types.py @@ -3,12 +3,17 @@ from enum import Enum from typing import Any, Callable, Dict, Optional, Set, Tuple, Type, TypeVar, Union from pydantic import BaseModel +from typing_extensions import Literal, TypeAlias DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any]) UnionType = getattr(types, "UnionType", Union) ModelNameMap = Dict[Union[Type[BaseModel], Type[Enum]], str] IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]] +EndpointDependencyScope: TypeAlias = Union[Literal["request", "function"], None] +LifespanDependencyScope: TypeAlias = Literal["lifespan"] +DependencyScope: TypeAlias = Union[LifespanDependencyScope, EndpointDependencyScope] + LifespanDependencyCacheKey = Union[ Tuple[Callable[..., Any], Union[str, int]], Callable[..., Any] ]