Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7ce51a7
♻️ Simplify internal classes for Depends and Security with dataclasses
tiangolo Oct 29, 2025
d0d20c2
🔥 Remove unnecessary repr test code for Depends as dataclasses handle…
tiangolo Oct 29, 2025
7e0ce06
♻️ Handle security depends possibly with None
tiangolo Oct 29, 2025
db77e81
🔥 Remove spoiler
tiangolo Oct 29, 2025
79d4853
♻️ Simplify and clean up internals for dependencies
tiangolo Oct 29, 2025
ae64f11
♻️ Reduce internal cyclic recursion in dependencies, from 2 functions…
tiangolo Oct 29, 2025
33a2edb
✨ Add scope to Depends parameters
tiangolo Oct 30, 2025
f302b2a
✨ Add scope logic to Dependant internal class
tiangolo Oct 30, 2025
1e0c2bc
✨ Add new exit async stack with scopes for function and request
tiangolo Oct 30, 2025
00b9028
✨ Add new error for invalid dependencies' scopes
tiangolo Oct 30, 2025
86ad30f
✨ Use new scoped async exit stacks for dependencies
tiangolo Oct 30, 2025
8c533ee
✅ Add some tests for dependencies with yield and scope
tiangolo Oct 30, 2025
9fe5d01
🔄 Merge branch 'master' into depends4
tiangolo Oct 30, 2025
786d059
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Oct 30, 2025
af0c1d4
🔄 Merge branch 'master' into depends4
tiangolo Oct 31, 2025
87bec29
♻️ Refactor dependencies, allow a default scope of None
tiangolo Nov 1, 2025
9532170
♻️ Refactor Dependant to compute its scope
tiangolo Nov 1, 2025
eef80ba
🔥 Remove unused imports
tiangolo Nov 1, 2025
936c1f4
♻️ Improve the error for broken scopes in dependencies
tiangolo Nov 1, 2025
0ca3d50
✅ Add more tests for dependencies with scopes
tiangolo Nov 1, 2025
d536c68
✅ Update tests for dependencies with yield and scopes
tiangolo Nov 1, 2025
f812dc5
✅ Add tests for dependencies with yield and scopes in websockets
tiangolo Nov 1, 2025
0989803
✅ Update tests to check dependencies are closed after websockets
tiangolo Nov 1, 2025
1c08488
🐛 Fix running WebSocket function inside both async exit stacks
tiangolo Nov 1, 2025
35adfad
✅ Add contextvars black magic state to handle multiple tests changing…
tiangolo Nov 1, 2025
334df87
✅ Tweak test for Python 3.8
tiangolo Nov 1, 2025
0fa09ee
🔥 Remove unnecessary comment
tiangolo Nov 1, 2025
c225644
📝 Update docs for dependencies with yield
tiangolo Nov 3, 2025
81e4d90
📝 Update param docs for `Depends()`
tiangolo Nov 3, 2025
781f137
📝 Add source examples for docs for dependencies with `yield` and scope
tiangolo Nov 3, 2025
c866bdb
✅ Add test for docs for dependencies with yield and scope
tiangolo Nov 3, 2025
a2d4374
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Nov 3, 2025
b0a7d59
📝 Update Mermaid chart
tiangolo Nov 3, 2025
90957d9
Merge branch 'master' into depends4
tiangolo Nov 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions docs/en/docs/tutorial/dependencies/dependencies-with-yield.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,51 @@ If you raise any exception in the code from the *path operation function*, it wi

///

## Early exit and `scope` { #early-exit-and-scope }

Normally the exit code of dependencies with `yield` is executed **after the response** is sent to the client.

But if you know that you won't need to use the dependency after returning from the *path operation function*, you can use `Depends(scope="function")` to tell FastAPI that it should close the dependency after the *path operation function* returns, but **before** the **response is sent**.

{* ../../docs_src/dependencies/tutorial008e_an_py39.py hl[12,16] *}

`Depends()` receives a `scope` parameter that can be:

* `"function"`: start the dependency before the *path operation function* that handles the request, end the dependency after the *path operation function* ends, but **before** the response is sent back to the client. So, the dependency function will be executed **around** the *path operation **function***.
* `"request"`: start the dependency before the *path operation function* that handles the request (similar to when using `"function"`), but end **after** the response is sent back to the client. So, the dependency function will be executed **around** the **request** and response cycle.

If not specified and the dependency has `yield`, it will have a `scope` of `"request"` by default.

### `scope` for sub-dependencies { #scope-for-sub-dependencies }

When you declare a dependency with a `scope="request"` (the default), any sub-dependency needs to also have a `scope` of `"request"`.

But a dependency with `scope` of `"function"` can have dependencies with `scope` of `"function"` and `scope` of `"request"`.

This is because any dependency needs to be able to run its exit code before the sub-dependencies, as it might need to still use them during its exit code.

```mermaid
sequenceDiagram

participant client as Client
participant dep_req as Dep scope="request"
participant dep_func as Dep scope="function"
participant operation as Path Operation

client ->> dep_req: Start request
Note over dep_req: Run code up to yield
dep_req ->> dep_func: Pass dependency
Note over dep_func: Run code up to yield
dep_func ->> operation: Run path operation with dependency
operation ->> dep_func: Return from path operation
Note over dep_func: Run code after yield
Note over dep_func: ✅ Dependency closed
dep_func ->> client: Send response to client
Note over client: Response sent
Note over dep_req: Run code after yield
Note over dep_req: ✅ Dependency closed
```

## Dependencies with `yield`, `HTTPException`, `except` and Background Tasks { #dependencies-with-yield-httpexception-except-and-background-tasks }

Dependencies with `yield` have evolved over time to cover different use cases and fix some issues.
Expand Down
15 changes: 15 additions & 0 deletions docs_src/dependencies/tutorial008e.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from fastapi import Depends, FastAPI

app = FastAPI()


def get_username():
try:
yield "Rick"
finally:
print("Cleanup up before response is sent")


@app.get("/users/me")
def get_user_me(username: str = Depends(get_username, scope="function")):
return username
16 changes: 16 additions & 0 deletions docs_src/dependencies/tutorial008e_an.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from fastapi import Depends, FastAPI
from typing_extensions import Annotated

app = FastAPI()


def get_username():
try:
yield "Rick"
finally:
print("Cleanup up before response is sent")


@app.get("/users/me")
def get_user_me(username: Annotated[str, Depends(get_username, scope="function")]):
return username
17 changes: 17 additions & 0 deletions docs_src/dependencies/tutorial008e_an_py39.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


def get_username():
try:
yield "Rick"
finally:
print("Cleanup up before response is sent")


@app.get("/users/me")
def get_user_me(username: Annotated[str, Depends(get_username, scope="function")]):
return username
54 changes: 50 additions & 4 deletions fastapi/dependencies/models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import inspect
import sys
from dataclasses import dataclass, field
from typing import Any, Callable, List, Optional, Sequence, Tuple
from functools import cached_property
from typing import Any, Callable, List, Optional, Sequence, Union

from fastapi._compat import ModelField
from fastapi.security.base import SecurityBase
from fastapi.types import DependencyCacheKey
from typing_extensions import Literal

if sys.version_info >= (3, 13): # pragma: no cover
from inspect import iscoroutinefunction
else: # pragma: no cover
from asyncio import iscoroutinefunction


@dataclass
Expand Down Expand Up @@ -31,7 +41,43 @@ class Dependant:
security_scopes: Optional[List[str]] = None
use_cache: bool = True
path: Optional[str] = None
cache_key: Tuple[Optional[Callable[..., Any]], Tuple[str, ...]] = field(init=False)
scope: Union[Literal["function", "request"], None] = None

@cached_property
def cache_key(self) -> DependencyCacheKey:
return (
self.call,
tuple(sorted(set(self.security_scopes or []))),
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)

def __post_init__(self) -> None:
self.cache_key = (self.call, tuple(sorted(set(self.security_scopes or []))))
@cached_property
def computed_scope(self) -> Union[str, None]:
if self.scope:
return self.scope
if self.is_gen_callable or self.is_async_gen_callable:
return "request"
return None
100 changes: 49 additions & 51 deletions fastapi/dependencies/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import inspect
import sys
from contextlib import AsyncExitStack, contextmanager
from copy import copy, deepcopy
from dataclasses import dataclass
Expand Down Expand Up @@ -55,10 +54,12 @@
contextmanager_in_threadpool,
)
from fastapi.dependencies.models import Dependant, SecurityRequirement
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.types import DependencyCacheKey
from fastapi.utils import create_model_field, get_path_param_names
from pydantic import BaseModel
from pydantic.fields import FieldInfo
Expand All @@ -74,15 +75,10 @@
from starlette.requests import HTTPConnection, Request
from starlette.responses import Response
from starlette.websockets import WebSocket
from typing_extensions import Annotated, get_args, get_origin
from typing_extensions import Annotated, Literal, get_args, get_origin

from .. import temp_pydantic_v1_params

if sys.version_info >= (3, 13): # pragma: no cover
from inspect import iscoroutinefunction
else: # pragma: no cover
from asyncio import iscoroutinefunction

multipart_not_installed_error = (
'Form data requires "python-multipart" to be installed. \n'
'You can install "python-multipart" with: \n\n'
Expand Down Expand Up @@ -137,14 +133,11 @@ def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> De
)


CacheKey = Tuple[Optional[Callable[..., Any]], Tuple[str, ...]]


def get_flat_dependant(
dependant: Dependant,
*,
skip_repeats: bool = False,
visited: Optional[List[CacheKey]] = None,
visited: Optional[List[DependencyCacheKey]] = None,
) -> Dependant:
if visited is None:
visited = []
Expand Down Expand Up @@ -237,21 +230,23 @@ def get_dependant(
name: Optional[str] = None,
security_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,
)
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
use_scopes = security_scopes or use_scopes
security_requirement = SecurityRequirement(
security_scheme=call, scopes=use_scopes
)
Expand All @@ -266,6 +261,16 @@ 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"
and param_details.depends.scope == "function"
):
assert dependant.call
raise DependencyScopeError(
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 []
if isinstance(param_details.depends, params.Security):
if param_details.depends.scopes:
Expand All @@ -276,6 +281,7 @@ def get_dependant(
name=param_name,
security_scopes=use_security_scopes,
use_cache=param_details.depends.use_cache,
scope=param_details.depends.scope,
)
dependant.dependencies.append(sub_dependant)
continue
Expand Down Expand Up @@ -532,36 +538,14 @@ def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None:
dependant.cookie_params.append(field)


def is_coroutine_callable(call: Callable[..., Any]) -> bool:
if inspect.isroutine(call):
return iscoroutinefunction(call)
if inspect.isclass(call):
return False
dunder_call = getattr(call, "__call__", None) # noqa: B004
return iscoroutinefunction(dunder_call)


def is_async_gen_callable(call: Callable[..., Any]) -> bool:
if inspect.isasyncgenfunction(call):
return True
dunder_call = getattr(call, "__call__", None) # noqa: B004
return inspect.isasyncgenfunction(dunder_call)


def is_gen_callable(call: Callable[..., Any]) -> bool:
if inspect.isgeneratorfunction(call):
return True
dunder_call = getattr(call, "__call__", None) # noqa: B004
return inspect.isgeneratorfunction(dunder_call)


async def solve_generator(
*, call: Callable[..., Any], stack: AsyncExitStack, sub_values: Dict[str, Any]
async def _solve_generator(
*, dependant: Dependant, stack: AsyncExitStack, sub_values: Dict[str, Any]
) -> Any:
if is_gen_callable(call):
cm = contextmanager_in_threadpool(contextmanager(call)(**sub_values))
elif is_async_gen_callable(call):
cm = asynccontextmanager(call)(**sub_values)
assert dependant.call
if dependant.is_gen_callable:
cm = contextmanager_in_threadpool(contextmanager(dependant.call)(**sub_values))
elif dependant.is_async_gen_callable:
cm = asynccontextmanager(dependant.call)(**sub_values)
return await stack.enter_async_context(cm)


Expand All @@ -571,7 +555,7 @@ class SolvedDependency:
errors: List[Any]
background_tasks: Optional[StarletteBackgroundTasks]
response: Response
dependency_cache: Dict[Tuple[Callable[..., Any], Tuple[str]], Any]
dependency_cache: Dict[DependencyCacheKey, Any]


async def solve_dependencies(
Expand All @@ -582,10 +566,20 @@ async def solve_dependencies(
background_tasks: Optional[StarletteBackgroundTasks] = None,
response: Optional[Response] = None,
dependency_overrides_provider: Optional[Any] = None,
dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None,
dependency_cache: Optional[Dict[DependencyCacheKey, Any]] = None,
# TODO: remove this parameter later, no longer used, not removing it yet as some
# people might be monkey patching this function (although that's not supported)
async_exit_stack: AsyncExitStack,
embed_body_fields: bool,
) -> SolvedDependency:
request_astack = request.scope.get("fastapi_inner_astack")
assert isinstance(request_astack, AsyncExitStack), (
"fastapi_inner_astack not found in request scope"
)
function_astack = request.scope.get("fastapi_function_astack")
assert isinstance(function_astack, AsyncExitStack), (
"fastapi_function_astack not found in request scope"
)
values: Dict[str, Any] = {}
errors: List[Any] = []
if response is None:
Expand All @@ -594,12 +588,8 @@ async def solve_dependencies(
response.status_code = None # type: ignore
if dependency_cache is None:
dependency_cache = {}
sub_dependant: Dependant
for sub_dependant in dependant.dependencies:
sub_dependant.call = cast(Callable[..., Any], sub_dependant.call)
sub_dependant.cache_key = cast(
Tuple[Callable[..., Any], Tuple[str]], sub_dependant.cache_key
)
call = sub_dependant.call
use_sub_dependant = sub_dependant
if (
Expand All @@ -616,6 +606,7 @@ async def solve_dependencies(
call=call,
name=sub_dependant.name,
security_scopes=sub_dependant.security_scopes,
scope=sub_dependant.scope,
)

solved_result = await solve_dependencies(
Expand All @@ -635,11 +626,18 @@ async def solve_dependencies(
continue
if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache:
solved = dependency_cache[sub_dependant.cache_key]
elif is_gen_callable(call) or is_async_gen_callable(call):
solved = await solve_generator(
call=call, stack=async_exit_stack, sub_values=solved_result.values
elif (
use_sub_dependant.is_gen_callable or use_sub_dependant.is_async_gen_callable
):
use_astack = request_astack
if sub_dependant.scope == "function":
use_astack = function_astack
solved = await _solve_generator(
dependant=use_sub_dependant,
stack=use_astack,
sub_values=solved_result.values,
)
elif is_coroutine_callable(call):
elif use_sub_dependant.is_coroutine_callable:
solved = await call(**solved_result.values)
else:
solved = await run_in_threadpool(call, **solved_result.values)
Expand Down
Loading