Compare commits

...
9 Commits
Author SHA1 Message Date
Sebastián Ramírezandgithub-actions[bot] ac068db630 🔖 Release version 0.140.5 (#16072)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-27 16:00:25 +00:00
github-actions[bot] 6be87d13a7 📝 Update release notes
[skip ci]
2026-07-27 15:53:30 +00:00
Sebastián Ramírez 23e4a10434 ️ Avoid flattening dependencies for body fields (#16071) 2026-07-27 15:52:55 +00:00
Sebastián Ramírezandgithub-actions[bot] 7ac0f1b541 🔖 Release version 0.140.4 (#16070)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-27 15:45:50 +00:00
github-actions[bot] 44e4eeaa78 📝 Update release notes
[skip ci]
2026-07-27 15:36:07 +00:00
Sebastián Ramírez 7134121a71 ️ Skip unused dependency repeat bookkeeping (#16069) 2026-07-27 15:35:29 +00:00
Sebastián Ramírezandgithub-actions[bot] 415d37fa6f 🔖 Release version 0.140.3 (#16068)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-27 15:16:54 +00:00
github-actions[bot] ca353d7215 📝 Update release notes
[skip ci]
2026-07-27 15:09:32 +00:00
Sebastián Ramírez d012979f9a ️ Avoid repeated dependency flattening in OpenAPI (#16067) 2026-07-27 15:08:45 +00:00
5 changed files with 83 additions and 46 deletions

No files matched your search

+18
View File
@@ -7,6 +7,24 @@ hide:
## Latest Changes
## 0.140.5 (2026-07-27)
### Refactors
* ⚡️ Avoid flattening dependencies for body fields. PR [#16071](https://github.com/fastapi/fastapi/pull/16071) by [@tiangolo](https://github.com/tiangolo).
## 0.140.4 (2026-07-27)
### Refactors
* ⚡️ Skip unused dependency repeat bookkeeping. PR [#16069](https://github.com/fastapi/fastapi/pull/16069) by [@tiangolo](https://github.com/tiangolo).
## 0.140.3 (2026-07-27)
### Refactors
* ⚡️ Avoid repeated dependency flattening in OpenAPI. PR [#16067](https://github.com/fastapi/fastapi/pull/16067) by [@tiangolo](https://github.com/tiangolo).
## 0.140.2 (2026-07-27)
### Refactors
+1 -1
View File
@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.140.2"
__version__ = "0.140.5"
from starlette import status as status
+42 -31
View File
@@ -152,16 +152,20 @@ def get_flat_dependant(
parent_oauth_scopes: list[str] | None = None,
_uses_scopes_cache: _UsesScopesCache | None = None,
) -> Dependant:
if visited is None:
visited = []
if _uses_scopes_cache is None:
_uses_scopes_cache = {}
visited.append(
_get_cache_key(
dependant=dependant,
uses_scopes_cache=_uses_scopes_cache,
)
track_visited = (
skip_repeats or visited is not None or _uses_scopes_cache is not None
)
if track_visited:
if visited is None:
visited = []
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 []) + (
_get_oauth_scopes(dependant=dependant)
)
@@ -187,15 +191,16 @@ def get_flat_dependant(
scope=dependant.scope,
)
for sub_dependant in dependant.dependencies:
if (
skip_repeats
and _get_cache_key(
dependant=sub_dependant,
uses_scopes_cache=_uses_scopes_cache,
)
in visited
):
continue
if skip_repeats:
assert visited is not None
if (
_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,
@@ -214,6 +219,16 @@ def get_flat_dependant(
return flat_dependant
def _get_flat_body_params(dependant: Dependant) -> list[ModelField]:
body_params: list[ModelField] = []
dependants = [dependant]
while dependants:
current_dependant = dependants.pop()
body_params.extend(current_dependant.body_params)
dependants.extend(reversed(current_dependant.dependencies))
return body_params
def _get_flat_fields_from_params(fields: list[ModelField]) -> list[ModelField]:
if not fields:
return fields
@@ -1038,8 +1053,8 @@ async def request_body_to_args(
return values, errors
def get_body_field(
*, flat_dependant: Dependant, name: str, embed_body_fields: bool
def _get_body_field(
*, body_params: list[ModelField], name: str, embed_body_fields: bool
) -> ModelField | None:
"""
Get a ModelField representing the request body for a path operation, combining
@@ -1051,34 +1066,30 @@ def get_body_field(
This is **not** used to validate/parse the request body, that's done with each
individual body parameter.
"""
if not flat_dependant.body_params:
if not body_params:
return None
first_param = flat_dependant.body_params[0]
first_param = body_params[0]
if not embed_body_fields:
return first_param
model_name = "Body_" + name
BodyModel = create_body_model(
fields=flat_dependant.body_params, model_name=model_name
)
required = any(
True for f in flat_dependant.body_params if f.field_info.is_required()
)
BodyModel = create_body_model(fields=body_params, model_name=model_name)
required = any(True for f in body_params if f.field_info.is_required())
BodyFieldInfo_kwargs: dict[str, Any] = {
"annotation": BodyModel,
"alias": "body",
}
if not required:
BodyFieldInfo_kwargs["default"] = None
if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params):
if any(isinstance(f.field_info, params.File) for f in body_params):
BodyFieldInfo: type[params.Body] = params.File
elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params):
elif any(isinstance(f.field_info, params.Form) for f in body_params):
BodyFieldInfo = params.Form
else:
BodyFieldInfo = params.Body
body_param_media_types = [
f.field_info.media_type
for f in flat_dependant.body_params
for f in body_params
if isinstance(f.field_info, params.Body)
]
if len(set(body_param_media_types)) == 1:
+13 -5
View File
@@ -112,7 +112,7 @@ def get_openapi_security_definitions(
def _get_openapi_operation_parameters(
*,
dependant: Dependant,
flat_dependant: Dependant,
model_name_map: ModelNameMap,
field_mapping: dict[
tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any]
@@ -120,7 +120,6 @@ def _get_openapi_operation_parameters(
separate_input_output_schemas: bool = True,
) -> list[dict[str, Any]]:
parameters = []
flat_dependant = get_flat_dependant(dependant, skip_repeats=True)
path_params = _get_flat_fields_from_params(flat_dependant.path_params)
query_params = _get_flat_fields_from_params(flat_dependant.query_params)
header_params = _get_flat_fields_from_params(flat_dependant.header_params)
@@ -284,12 +283,22 @@ def get_openapi_path(
assert current_response_class, "A response class is needed to generate OpenAPI"
route_response_media_type: str | None = current_response_class.media_type
if route.include_in_schema:
flat_dependant = get_flat_dependant(route.dependant, skip_repeats=True)
all_route_params = [
field
for fields in (
flat_dependant.path_params,
flat_dependant.query_params,
flat_dependant.header_params,
flat_dependant.cookie_params,
)
for field in _get_flat_fields_from_params(fields)
]
for method in route.methods:
operation = get_openapi_operation_metadata(
route=route, method=method, operation_ids=operation_ids
)
parameters: list[dict[str, Any]] = []
flat_dependant = get_flat_dependant(route.dependant, skip_repeats=True)
security_definitions, operation_security = get_openapi_security_definitions(
flat_dependant=flat_dependant
)
@@ -298,7 +307,7 @@ def get_openapi_path(
if security_definitions:
security_schemes.update(security_definitions)
operation_parameters = _get_openapi_operation_parameters(
dependant=route.dependant,
flat_dependant=flat_dependant,
model_name_map=model_name_map,
field_mapping=field_mapping,
separate_input_output_schemas=separate_input_output_schemas,
@@ -458,7 +467,6 @@ def get_openapi_path(
deep_dict_update(openapi_response, process_response)
openapi_response["description"] = description
http422 = "422"
all_route_params = get_flat_params(route.dependant)
if (all_route_params or route.body_field) and not any(
status in operation["responses"]
for status in [http422, "4XX", "default"]
+9 -9
View File
@@ -55,10 +55,10 @@ from fastapi.dependencies.models import (
_is_gen_callable,
)
from fastapi.dependencies.utils import (
_get_body_field,
_get_flat_body_params,
_should_embed_body_fields,
get_body_field,
get_dependant,
get_flat_dependant,
get_parameterless_sub_dependant,
get_stream_item_type,
get_typed_return_annotation,
@@ -849,16 +849,16 @@ def _build_dependant_with_parameterless_dependencies(
path: str,
call: Callable[..., Any],
dependencies: Sequence[params.Depends],
) -> tuple[Dependant, Dependant, bool]:
) -> tuple[Dependant, list[ModelField], bool]:
dependant = get_dependant(path=path, call=call, scope="function")
for depends in dependencies[::-1]:
dependant.dependencies.insert(
0,
get_parameterless_sub_dependant(depends=depends, path=path),
)
flat_dependant = get_flat_dependant(dependant)
embed_body_fields = _should_embed_body_fields(flat_dependant.body_params)
return dependant, flat_dependant, embed_body_fields
body_params = _get_flat_body_params(dependant)
embed_body_fields = _should_embed_body_fields(body_params)
return dependant, body_params, embed_body_fields
class _RouteWithPath(Protocol):
@@ -1090,15 +1090,15 @@ def _populate_api_route_state(
assert callable(endpoint), "An endpoint must be a callable"
(
route.dependant,
flat_dependant,
body_params,
route._embed_body_fields,
) = _build_dependant_with_parameterless_dependencies(
path=route.path_format,
call=route.endpoint,
dependencies=route.dependencies,
)
route.body_field = get_body_field(
flat_dependant=flat_dependant,
route.body_field = _get_body_field(
body_params=body_params,
name=route.unique_id,
embed_body_fields=route._embed_body_fields,
)