Ensure functions have docstrings for documentation
def ensure_multipart_is_installed() -> None:
1import dataclasses2import inspect3import sys4from collections.abc import (5 AsyncGenerator,6 AsyncIterable,7 AsyncIterator,8 Callable,9 Generator,10 Iterable,11 Iterator,12 Mapping,13 Sequence,14)15from contextlib import AsyncExitStack, contextmanager16from copy import copy, deepcopy17from dataclasses import dataclass18from typing import (19 Annotated,20 Any,21 ForwardRef,22 Literal,23 Union,24 cast,25 get_args,26 get_origin,27)2829from fastapi import params30from fastapi._compat import (31 ModelField,32 RequiredParam,33 Undefined,34 copy_field_info,35 create_body_model,36 evaluate_forwardref,37 field_annotation_is_scalar,38 field_annotation_is_scalar_sequence,39 field_annotation_is_sequence,40 get_cached_model_fields,41 get_missing_field_error,42 is_bytes_or_nonable_bytes_annotation,43 is_bytes_sequence_annotation,44 is_scalar_field,45 is_uploadfile_or_nonable_uploadfile_annotation,46 is_uploadfile_sequence_annotation,47 lenient_issubclass,48 sequence_types,49 serialize_sequence_value,50 value_is_sequence,51)52from fastapi.background import BackgroundTasks53from fastapi.concurrency import (54 asynccontextmanager,55 contextmanager_in_threadpool,56)57from fastapi.dependencies.models import Dependant58from fastapi.exceptions import DependencyScopeError59from fastapi.logger import logger60from fastapi.security.oauth2 import SecurityScopes61from fastapi.types import DependencyCacheKey62from fastapi.utils import create_model_field, get_path_param_names63from pydantic import BaseModel, Json64from pydantic.fields import FieldInfo65from starlette.background import BackgroundTasks as StarletteBackgroundTasks66from starlette.concurrency import run_in_threadpool67from starlette.datastructures import (68 FormData,69 Headers,70 ImmutableMultiDict,71 QueryParams,72 UploadFile,73)74from starlette.requests import HTTPConnection, Request75from starlette.responses import Response76from starlette.websockets import WebSocket77from typing_inspection.typing_objects import is_typealiastype7879multipart_not_installed_error = (80 'Form data requires "python-multipart" to be installed. \n'81 'You can install "python-multipart" with: \n\n'82 "pip install python-multipart\n"83)84multipart_incorrect_install_error = (85 'Form data requires "python-multipart" to be installed. '86 'It seems you installed "multipart" instead. \n'87 'You can remove "multipart" with: \n\n'88 "pip uninstall multipart\n\n"89 'And then install "python-multipart" with: \n\n'90 "pip install python-multipart\n"91)929394def ensure_multipart_is_installed() -> None:95 try:96 from python_multipart import __version__9798 # Import an attribute that can be mocked/deleted in testing99 assert __version__ > "0.0.12"100 except (ImportError, AssertionError):101 try:102 # __version__ is available in both multiparts, and can be mocked103 from multipart import ( # type: ignore[no-redef,import-untyped]104 __version__,105 )106107 assert __version__108 try:109 # parse_options_header is only available in the right multipart110 from multipart.multipart import ( # type: ignore[import-untyped]111 parse_options_header,112 )113114 assert parse_options_header115 except ImportError:116 logger.error(multipart_incorrect_install_error)117 raise RuntimeError(multipart_incorrect_install_error) from None118 except ImportError:119 logger.error(multipart_not_installed_error)120 raise RuntimeError(multipart_not_installed_error) from None121122123def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> Dependant:124 assert callable(depends.dependency), (125 "A parameter-less dependency must have a callable dependency"126 )127 own_oauth_scopes: list[str] = []128 if isinstance(depends, params.Security) and depends.scopes:129 own_oauth_scopes.extend(depends.scopes)130 return get_dependant(131 path=path,132 call=depends.dependency,133 scope=depends.scope,134 own_oauth_scopes=own_oauth_scopes,135 )136137138def get_flat_dependant(139 dependant: Dependant,140 *,141 skip_repeats: bool = False,142 visited: list[DependencyCacheKey] | None = None,143 parent_oauth_scopes: list[str] | None = None,144) -> Dependant:145 if visited is None:146 visited = []147 visited.append(dependant.cache_key)148 use_parent_oauth_scopes = (parent_oauth_scopes or []) + (149 dependant.oauth_scopes or []150 )151152 flat_dependant = Dependant(153 path_params=dependant.path_params.copy(),154 query_params=dependant.query_params.copy(),155 header_params=dependant.header_params.copy(),156 cookie_params=dependant.cookie_params.copy(),157 body_params=dependant.body_params.copy(),158 name=dependant.name,159 call=dependant.call,160 request_param_name=dependant.request_param_name,161 websocket_param_name=dependant.websocket_param_name,162 http_connection_param_name=dependant.http_connection_param_name,163 response_param_name=dependant.response_param_name,164 background_tasks_param_name=dependant.background_tasks_param_name,165 security_scopes_param_name=dependant.security_scopes_param_name,166 own_oauth_scopes=dependant.own_oauth_scopes,167 parent_oauth_scopes=use_parent_oauth_scopes,168 use_cache=dependant.use_cache,169 path=dependant.path,170 scope=dependant.scope,171 )172 for sub_dependant in dependant.dependencies:173 if skip_repeats and sub_dependant.cache_key in visited:174 continue175 flat_sub = get_flat_dependant(176 sub_dependant,177 skip_repeats=skip_repeats,178 visited=visited,179 parent_oauth_scopes=flat_dependant.oauth_scopes,180 )181 flat_dependant.dependencies.append(flat_sub)182 flat_dependant.path_params.extend(flat_sub.path_params)183 flat_dependant.query_params.extend(flat_sub.query_params)184 flat_dependant.header_params.extend(flat_sub.header_params)185 flat_dependant.cookie_params.extend(flat_sub.cookie_params)186 flat_dependant.body_params.extend(flat_sub.body_params)187 flat_dependant.dependencies.extend(flat_sub.dependencies)188189 return flat_dependant190191192def _get_flat_fields_from_params(fields: list[ModelField]) -> list[ModelField]:193 if not fields:194 return fields195 first_field = fields[0]196 if len(fields) == 1 and lenient_issubclass(197 first_field.field_info.annotation, BaseModel198 ):199 fields_to_extract = get_cached_model_fields(first_field.field_info.annotation)200 return fields_to_extract201 return fields202203204def get_flat_params(dependant: Dependant) -> list[ModelField]:205 flat_dependant = get_flat_dependant(dependant, skip_repeats=True)206 path_params = _get_flat_fields_from_params(flat_dependant.path_params)207 query_params = _get_flat_fields_from_params(flat_dependant.query_params)208 header_params = _get_flat_fields_from_params(flat_dependant.header_params)209 cookie_params = _get_flat_fields_from_params(flat_dependant.cookie_params)210 return path_params + query_params + header_params + cookie_params211212213def _get_signature(call: Callable[..., Any]) -> inspect.Signature:214 try:215 signature = inspect.signature(call, eval_str=True)216 except NameError:217 # Handle type annotations with if TYPE_CHECKING, not used by FastAPI218 # e.g. dependency return types219 if sys.version_info >= (3, 14):220 from annotationlib import Format221222 signature = inspect.signature(call, annotation_format=Format.FORWARDREF)223 else:224 signature = inspect.signature(call)225 return signature226227228def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:229 signature = _get_signature(call)230 unwrapped = inspect.unwrap(call)231 globalns = getattr(unwrapped, "__globals__", {})232 typed_params = [233 inspect.Parameter(234 name=param.name,235 kind=param.kind,236 default=param.default,237 annotation=get_typed_annotation(param.annotation, globalns),238 )239 for param in signature.parameters.values()240 ]241 typed_signature = inspect.Signature(typed_params)242 return typed_signature243244245def get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any:246 if isinstance(annotation, str):247 annotation = ForwardRef(annotation)248 annotation = evaluate_forwardref(annotation, globalns, globalns)249 if annotation is type(None):250 return None251 return annotation252253254def get_typed_return_annotation(call: Callable[..., Any]) -> Any:255 signature = _get_signature(call)256 unwrapped = inspect.unwrap(call)257 annotation = signature.return_annotation258259 if annotation is inspect.Signature.empty:260 return None261262 globalns = getattr(unwrapped, "__globals__", {})263 return get_typed_annotation(annotation, globalns)264265266_STREAM_ORIGINS = {267 AsyncIterable,268 AsyncIterator,269 AsyncGenerator,270 Iterable,271 Iterator,272 Generator,273}274275276def get_stream_item_type(annotation: Any) -> Any | None:277 origin = get_origin(annotation)278 if origin is not None and origin in _STREAM_ORIGINS:279 type_args = get_args(annotation)280 if type_args:281 return type_args[0]282 return Any283 return None284285286def get_dependant(287 *,288 path: str,289 call: Callable[..., Any],290 name: str | None = None,291 own_oauth_scopes: list[str] | None = None,292 parent_oauth_scopes: list[str] | None = None,293 use_cache: bool = True,294 scope: Literal["function", "request"] | None = None,295) -> Dependant:296 dependant = Dependant(297 call=call,298 name=name,299 path=path,300 use_cache=use_cache,301 scope=scope,302 own_oauth_scopes=own_oauth_scopes,303 parent_oauth_scopes=parent_oauth_scopes,304 )305 current_scopes = (parent_oauth_scopes or []) + (own_oauth_scopes or [])306 path_param_names = get_path_param_names(path)307 endpoint_signature = get_typed_signature(call)308 signature_params = endpoint_signature.parameters309 for param_name, param in signature_params.items():310 is_path_param = param_name in path_param_names311 param_details = analyze_param(312 param_name=param_name,313 annotation=param.annotation,314 value=param.default,315 is_path_param=is_path_param,316 )317 if param_details.depends is not None:318 assert param_details.depends.dependency319 if (320 (dependant.is_gen_callable or dependant.is_async_gen_callable)321 and dependant.computed_scope == "request"322 and param_details.depends.scope == "function"323 ):324 assert dependant.call325 call_name = getattr(dependant.call, "__name__", "<unnamed_callable>")326 raise DependencyScopeError(327 f'The dependency "{call_name}" has a scope of '328 '"request", it cannot depend on dependencies with scope "function".'329 )330 sub_own_oauth_scopes: list[str] = []331 if isinstance(param_details.depends, params.Security):332 if param_details.depends.scopes:333 sub_own_oauth_scopes = list(param_details.depends.scopes)334 sub_dependant = get_dependant(335 path=path,336 call=param_details.depends.dependency,337 name=param_name,338 own_oauth_scopes=sub_own_oauth_scopes,339 parent_oauth_scopes=current_scopes,340 use_cache=param_details.depends.use_cache,341 scope=param_details.depends.scope,342 )343 dependant.dependencies.append(sub_dependant)344 continue345 if add_non_field_param_to_dependency(346 param_name=param_name,347 type_annotation=param_details.type_annotation,348 dependant=dependant,349 ):350 assert param_details.field is None, (351 f"Cannot specify multiple FastAPI annotations for {param_name!r}"352 )353 continue354 assert param_details.field is not None355 if isinstance(param_details.field.field_info, params.Body):356 dependant.body_params.append(param_details.field)357 else:358 add_param_to_fields(field=param_details.field, dependant=dependant)359 return dependant360361362def add_non_field_param_to_dependency(363 *, param_name: str, type_annotation: Any, dependant: Dependant364) -> bool | None:365 if lenient_issubclass(type_annotation, Request):366 dependant.request_param_name = param_name367 return True368 elif lenient_issubclass(type_annotation, WebSocket):369 dependant.websocket_param_name = param_name370 return True371 elif lenient_issubclass(type_annotation, HTTPConnection):372 dependant.http_connection_param_name = param_name373 return True374 elif lenient_issubclass(type_annotation, Response):375 dependant.response_param_name = param_name376 return True377 elif lenient_issubclass(type_annotation, StarletteBackgroundTasks):378 dependant.background_tasks_param_name = param_name379 return True380 elif lenient_issubclass(type_annotation, SecurityScopes):381 dependant.security_scopes_param_name = param_name382 return True383 return None384385386@dataclass387class ParamDetails:388 type_annotation: Any389 depends: params.Depends | None390 field: ModelField | None391392393def analyze_param(394 *,395 param_name: str,396 annotation: Any,397 value: Any,398 is_path_param: bool,399) -> ParamDetails:400 field_info = None401 depends = None402 type_annotation: Any = Any403 use_annotation: Any = Any404 if is_typealiastype(annotation):405 # unpack in case PEP 695 type syntax is used406 annotation = annotation.__value__407 if annotation is not inspect.Signature.empty:408 use_annotation = annotation409 type_annotation = annotation410 # Extract Annotated info411 if get_origin(use_annotation) is Annotated:412 annotated_args = get_args(annotation)413 type_annotation = annotated_args[0]414 fastapi_annotations = [415 arg416 for arg in annotated_args[1:]417 if isinstance(arg, (FieldInfo, params.Depends))418 ]419 fastapi_specific_annotations = [420 arg421 for arg in fastapi_annotations422 if isinstance(423 arg,424 (425 params.Param,426 params.Body,427 params.Depends,428 ),429 )430 ]431 if fastapi_specific_annotations:432 fastapi_annotation: FieldInfo | params.Depends | None = (433 fastapi_specific_annotations[-1]434 )435 else:436 fastapi_annotation = None437 # Set default for Annotated FieldInfo438 if isinstance(fastapi_annotation, FieldInfo):439 # Copy `field_info` because we mutate `field_info.default` below.440 field_info = copy_field_info(441 field_info=fastapi_annotation,442 annotation=use_annotation,443 )444 assert (445 field_info.default == Undefined or field_info.default == RequiredParam446 ), (447 f"`{field_info.__class__.__name__}` default value cannot be set in"448 f" `Annotated` for {param_name!r}. Set the default value with `=` instead."449 )450 if value is not inspect.Signature.empty:451 assert not is_path_param, "Path parameters cannot have default values"452 field_info.default = value453 else:454 field_info.default = RequiredParam455 # Get Annotated Depends456 elif isinstance(fastapi_annotation, params.Depends):457 depends = fastapi_annotation458 # Get Depends from default value459 if isinstance(value, params.Depends):460 assert depends is None, (461 "Cannot specify `Depends` in `Annotated` and default value"462 f" together for {param_name!r}"463 )464 assert field_info is None, (465 "Cannot specify a FastAPI annotation in `Annotated` and `Depends` as a"466 f" default value together for {param_name!r}"467 )468 depends = value469 # Get FieldInfo from default value470 elif isinstance(value, FieldInfo):471 assert field_info is None, (472 "Cannot specify FastAPI annotations in `Annotated` and default value"473 f" together for {param_name!r}"474 )475 field_info = value476 if isinstance(field_info, FieldInfo):477 field_info.annotation = type_annotation478479 # Get Depends from type annotation480 if depends is not None and depends.dependency is None:481 # Copy `depends` before mutating it482 depends = copy(depends)483 depends = dataclasses.replace(depends, dependency=type_annotation)484485 # Handle non-param type annotations like Request486 # Only apply special handling when there's no explicit Depends - if there's a Depends,487 # the dependency will be called and its return value used instead of the special injection488 if depends is None and lenient_issubclass(489 type_annotation,490 (491 Request,492 WebSocket,493 HTTPConnection,494 Response,495 StarletteBackgroundTasks,496 SecurityScopes,497 ),498 ):499 assert field_info is None, (500 f"Cannot specify FastAPI annotation for type {type_annotation!r}"501 )502 # Handle default assignations, neither field_info nor depends was not found in Annotated nor default value503 elif field_info is None and depends is None:504 default_value = value if value is not inspect.Signature.empty else RequiredParam505 if is_path_param:506 # We might check here that `default_value is RequiredParam`, but the fact is that the same507 # parameter might sometimes be a path parameter and sometimes not. See508 # `tests/test_infer_param_optionality.py` for an example.509 field_info = params.Path(annotation=use_annotation)510 elif is_uploadfile_or_nonable_uploadfile_annotation(511 type_annotation512 ) or is_uploadfile_sequence_annotation(type_annotation):513 field_info = params.File(annotation=use_annotation, default=default_value)514 elif not field_annotation_is_scalar(annotation=type_annotation):515 field_info = params.Body(annotation=use_annotation, default=default_value)516 else:517 field_info = params.Query(annotation=use_annotation, default=default_value)518519 field = None520 # It's a field_info, not a dependency521 if field_info is not None:522 # Handle field_info.in_523 if is_path_param:524 assert isinstance(field_info, params.Path), (525 f"Cannot use `{field_info.__class__.__name__}` for path param"526 f" {param_name!r}"527 )528 elif (529 isinstance(field_info, params.Param)530 and getattr(field_info, "in_", None) is None531 ):532 field_info.in_ = params.ParamTypes.query533 use_annotation_from_field_info = use_annotation534 if isinstance(field_info, params.Form):535 ensure_multipart_is_installed()536 if not field_info.alias and getattr(field_info, "convert_underscores", None):537 alias = param_name.replace("_", "-")538 else:539 alias = field_info.alias or param_name540 field_info.alias = alias541 field = create_model_field(542 name=param_name,543 type_=use_annotation_from_field_info,544 default=field_info.default,545 alias=alias,546 field_info=field_info,547 )548 if is_path_param:549 assert is_scalar_field(field=field), (550 "Path params must be of one of the supported types"551 )552 elif isinstance(field_info, params.Query):553 assert (554 is_scalar_field(field)555 or field_annotation_is_scalar_sequence(field.field_info.annotation)556 or lenient_issubclass(field.field_info.annotation, BaseModel)557 ), f"Query parameter {param_name!r} must be one of the supported types"558559 return ParamDetails(type_annotation=type_annotation, depends=depends, field=field)560561562def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None:563 field_info = field.field_info564 field_info_in = getattr(field_info, "in_", None)565 if field_info_in == params.ParamTypes.path:566 dependant.path_params.append(field)567 elif field_info_in == params.ParamTypes.query:568 dependant.query_params.append(field)569 elif field_info_in == params.ParamTypes.header:570 dependant.header_params.append(field)571 else:572 assert field_info_in == params.ParamTypes.cookie, (573 f"non-body parameters must be in path, query, header or cookie: {field.name}"574 )575 dependant.cookie_params.append(field)576577578async def _solve_generator(579 *, dependant: Dependant, stack: AsyncExitStack, sub_values: dict[str, Any]580) -> Any:581 assert dependant.call582 if dependant.is_async_gen_callable:583 cm = asynccontextmanager(dependant.call)(**sub_values)584 elif dependant.is_gen_callable:585 cm = contextmanager_in_threadpool(contextmanager(dependant.call)(**sub_values))586 return await stack.enter_async_context(cm)587588589@dataclass590class SolvedDependency:591 values: dict[str, Any]592 errors: list[Any]593 background_tasks: StarletteBackgroundTasks | None594 response: Response595 dependency_cache: dict[DependencyCacheKey, Any]596597598async def solve_dependencies(599 *,600 request: Request | WebSocket,601 dependant: Dependant,602 body: dict[str, Any] | FormData | bytes | None = None,603 background_tasks: StarletteBackgroundTasks | None = None,604 response: Response | None = None,605 dependency_overrides_provider: Any | None = None,606 dependency_cache: dict[DependencyCacheKey, Any] | None = None,607 # TODO: remove this parameter later, no longer used, not removing it yet as some608 # people might be monkey patching this function (although that's not supported)609 async_exit_stack: AsyncExitStack,610 embed_body_fields: bool,611) -> SolvedDependency:612 request_astack = request.scope.get("fastapi_inner_astack")613 assert isinstance(request_astack, AsyncExitStack), (614 "fastapi_inner_astack not found in request scope"615 )616 function_astack = request.scope.get("fastapi_function_astack")617 assert isinstance(function_astack, AsyncExitStack), (618 "fastapi_function_astack not found in request scope"619 )620 values: dict[str, Any] = {}621 errors: list[Any] = []622 if response is None:623 response = Response()624 del response.headers["content-length"]625 response.status_code = None # type: ignore626 if dependency_cache is None:627 dependency_cache = {}628 for sub_dependant in dependant.dependencies:629 sub_dependant.call = cast(Callable[..., Any], sub_dependant.call)630 call = sub_dependant.call631 use_sub_dependant = sub_dependant632 if (633 dependency_overrides_provider634 and dependency_overrides_provider.dependency_overrides635 ):636 original_call = sub_dependant.call637 call = getattr(638 dependency_overrides_provider, "dependency_overrides", {}639 ).get(original_call, original_call)640 use_path: str = sub_dependant.path # type: ignore641 use_sub_dependant = get_dependant(642 path=use_path,643 call=call,644 name=sub_dependant.name,645 parent_oauth_scopes=sub_dependant.oauth_scopes,646 scope=sub_dependant.scope,647 )648649 solved_result = await solve_dependencies(650 request=request,651 dependant=use_sub_dependant,652 body=body,653 background_tasks=background_tasks,654 response=response,655 dependency_overrides_provider=dependency_overrides_provider,656 dependency_cache=dependency_cache,657 async_exit_stack=async_exit_stack,658 embed_body_fields=embed_body_fields,659 )660 background_tasks = solved_result.background_tasks661 if solved_result.errors:662 errors.extend(solved_result.errors)663 continue664 if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache:665 solved = dependency_cache[sub_dependant.cache_key]666 elif (667 use_sub_dependant.is_gen_callable or use_sub_dependant.is_async_gen_callable668 ):669 use_astack = request_astack670 if sub_dependant.scope == "function":671 use_astack = function_astack672 solved = await _solve_generator(673 dependant=use_sub_dependant,674 stack=use_astack,675 sub_values=solved_result.values,676 )677 elif use_sub_dependant.is_coroutine_callable:678 solved = await call(**solved_result.values)679 else:680 solved = await run_in_threadpool(call, **solved_result.values)681 if sub_dependant.name is not None:682 values[sub_dependant.name] = solved683 if sub_dependant.cache_key not in dependency_cache:684 dependency_cache[sub_dependant.cache_key] = solved685 path_values, path_errors = request_params_to_args(686 dependant.path_params, request.path_params687 )688 query_values, query_errors = request_params_to_args(689 dependant.query_params, request.query_params690 )691 header_values, header_errors = request_params_to_args(692 dependant.header_params, request.headers693 )694 cookie_values, cookie_errors = request_params_to_args(695 dependant.cookie_params, request.cookies696 )697 values.update(path_values)698 values.update(query_values)699 values.update(header_values)700 values.update(cookie_values)701 errors += path_errors + query_errors + header_errors + cookie_errors702 if dependant.body_params:703 (704 body_values,705 body_errors,706 ) = await request_body_to_args( # body_params checked above707 body_fields=dependant.body_params,708 received_body=body,709 embed_body_fields=embed_body_fields,710 )711 values.update(body_values)712 errors.extend(body_errors)713 if dependant.http_connection_param_name:714 values[dependant.http_connection_param_name] = request715 if dependant.request_param_name and isinstance(request, Request):716 values[dependant.request_param_name] = request717 elif dependant.websocket_param_name and isinstance(request, WebSocket):718 values[dependant.websocket_param_name] = request719 if dependant.background_tasks_param_name:720 if background_tasks is None:721 background_tasks = BackgroundTasks()722 values[dependant.background_tasks_param_name] = background_tasks723 if dependant.response_param_name:724 values[dependant.response_param_name] = response725 if dependant.security_scopes_param_name:726 values[dependant.security_scopes_param_name] = SecurityScopes(727 scopes=dependant.oauth_scopes728 )729 return SolvedDependency(730 values=values,731 errors=errors,732 background_tasks=background_tasks,733 response=response,734 dependency_cache=dependency_cache,735 )736737738def _validate_value_with_model_field(739 *, field: ModelField, value: Any, values: dict[str, Any], loc: tuple[str, ...]740) -> tuple[Any, list[Any]]:741 if value is None:742 if field.field_info.is_required():743 return None, [get_missing_field_error(loc=loc)]744 else:745 return deepcopy(field.default), []746 return field.validate(value, values, loc=loc)747748749def _is_json_field(field: ModelField) -> bool:750 return any(type(item) is Json for item in field.field_info.metadata)751752753def _get_multidict_value(754 field: ModelField, values: Mapping[str, Any], alias: str | None = None755) -> Any:756 alias = alias or get_validation_alias(field)757 if (758 (not _is_json_field(field))759 and field_annotation_is_sequence(field.field_info.annotation)760 and isinstance(values, (ImmutableMultiDict, Headers))761 ):762 value = values.getlist(alias)763 else:764 value = values.get(alias, None)765 if (766 value is None767 or (768 isinstance(field.field_info, params.Form)769 and isinstance(value, str) # For type checks770 and value == ""771 )772 or (773 field_annotation_is_sequence(field.field_info.annotation)774 and len(value) == 0775 )776 ):777 if field.field_info.is_required():778 return779 else:780 return deepcopy(field.default)781 return value782783784def request_params_to_args(785 fields: Sequence[ModelField],786 received_params: Mapping[str, Any] | QueryParams | Headers,787) -> tuple[dict[str, Any], list[Any]]:788 values: dict[str, Any] = {}789 errors: list[dict[str, Any]] = []790791 if not fields:792 return values, errors793794 first_field = fields[0]795 fields_to_extract = fields796 single_not_embedded_field = False797 default_convert_underscores = True798 if len(fields) == 1 and lenient_issubclass(799 first_field.field_info.annotation, BaseModel800 ):801 fields_to_extract = get_cached_model_fields(first_field.field_info.annotation)802 single_not_embedded_field = True803 # If headers are in a Pydantic model, the way to disable convert_underscores804 # would be with Header(convert_underscores=False) at the Pydantic model level805 default_convert_underscores = getattr(806 first_field.field_info, "convert_underscores", True807 )808809 params_to_process: dict[str, Any] = {}810811 processed_keys = set()812813 for field in fields_to_extract:814 alias = None815 if isinstance(received_params, Headers):816 # Handle fields extracted from a Pydantic Model for a header, each field817 # doesn't have a FieldInfo of type Header with the default convert_underscores=True818 convert_underscores = getattr(819 field.field_info, "convert_underscores", default_convert_underscores820 )821 if convert_underscores:822 alias = get_validation_alias(field)823 if alias == field.name:824 alias = alias.replace("_", "-")825 value = _get_multidict_value(field, received_params, alias=alias)826 if value is not None:827 params_to_process[get_validation_alias(field)] = value828 processed_keys.add(alias or get_validation_alias(field))829 # For headers with convert_underscores=True, mark both the converted830 # header name and the original field alias as processed to avoid831 # accepting the original alias as an extra header.832 processed_keys.add(get_validation_alias(field))833834 for key in received_params.keys():835 if key not in processed_keys:836 if isinstance(received_params, (ImmutableMultiDict, Headers)):837 value = received_params.getlist(key)838 if isinstance(value, list) and (len(value) == 1):839 params_to_process[key] = value[0]840 else:841 params_to_process[key] = value842 else:843 params_to_process[key] = received_params.get(key)844845 if single_not_embedded_field:846 field_info = first_field.field_info847 assert isinstance(field_info, params.Param), (848 "Params must be subclasses of Param"849 )850 loc: tuple[str, ...] = (field_info.in_.value,)851 v_, errors_ = _validate_value_with_model_field(852 field=first_field, value=params_to_process, values=values, loc=loc853 )854 return {first_field.name: v_}, errors_855856 for field in fields:857 value = _get_multidict_value(field, received_params)858 field_info = field.field_info859 assert isinstance(field_info, params.Param), (860 "Params must be subclasses of Param"861 )862 loc = (field_info.in_.value, get_validation_alias(field))863 v_, errors_ = _validate_value_with_model_field(864 field=field, value=value, values=values, loc=loc865 )866 if errors_:867 errors.extend(errors_)868 else:869 values[field.name] = v_870 return values, errors871872873def is_union_of_base_models(field_type: Any) -> bool:874 """Check if field type is a Union where all members are BaseModel subclasses."""875 from fastapi.types import UnionType876877 origin = get_origin(field_type)878879 # Check if it's a Union type (covers both typing.Union and types.UnionType in Python 3.10+)880 if origin is not Union and origin is not UnionType:881 return False882883 union_args = get_args(field_type)884885 for arg in union_args:886 if not lenient_issubclass(arg, BaseModel):887 return False888889 return True890891892def _should_embed_body_fields(fields: list[ModelField]) -> bool:893 if not fields:894 return False895 # More than one dependency could have the same field, it would show up as multiple896 # fields but it's the same one, so count them by name897 body_param_names_set = {field.name for field in fields}898 # A top level field has to be a single field, not multiple899 if len(body_param_names_set) > 1:900 return True901 first_field = fields[0]902 # If it explicitly specifies it is embedded, it has to be embedded903 if getattr(first_field.field_info, "embed", None):904 return True905 # If it's a Form (or File) field, it has to be a BaseModel (or a union of BaseModels) to be top level906 # otherwise it has to be embedded, so that the key value pair can be extracted907 if (908 isinstance(first_field.field_info, params.Form)909 and not lenient_issubclass(first_field.field_info.annotation, BaseModel)910 and not is_union_of_base_models(first_field.field_info.annotation)911 ):912 return True913 return False914915916async def _extract_form_body(917 body_fields: list[ModelField],918 received_body: FormData,919) -> dict[str, Any]:920 values = {}921922 for field in body_fields:923 value = _get_multidict_value(field, received_body)924 field_info = field.field_info925 if (926 isinstance(field_info, params.File)927 and is_bytes_or_nonable_bytes_annotation(field.field_info.annotation)928 and isinstance(value, UploadFile)929 ):930 value = await value.read()931 elif (932 is_bytes_sequence_annotation(field.field_info.annotation)933 and isinstance(field_info, params.File)934 and value_is_sequence(value)935 ):936 # For types937 assert isinstance(value, sequence_types)938 results: list[bytes | str] = []939 for sub_value in value:940 results.append(await sub_value.read())941 value = serialize_sequence_value(field=field, value=results)942 if value is not None:943 values[get_validation_alias(field)] = value944 field_aliases = {get_validation_alias(field) for field in body_fields}945 for key in received_body.keys():946 if key not in field_aliases:947 param_values = received_body.getlist(key)948 if len(param_values) == 1:949 values[key] = param_values[0]950 else:951 values[key] = param_values952 return values953954955async def request_body_to_args(956 body_fields: list[ModelField],957 received_body: dict[str, Any] | FormData | bytes | None,958 embed_body_fields: bool,959) -> tuple[dict[str, Any], list[dict[str, Any]]]:960 values: dict[str, Any] = {}961 errors: list[dict[str, Any]] = []962 assert body_fields, "request_body_to_args() should be called with fields"963 single_not_embedded_field = len(body_fields) == 1 and not embed_body_fields964 first_field = body_fields[0]965 body_to_process = received_body966967 fields_to_extract: list[ModelField] = body_fields968969 if (970 single_not_embedded_field971 and lenient_issubclass(first_field.field_info.annotation, BaseModel)972 and isinstance(received_body, FormData)973 ):974 fields_to_extract = get_cached_model_fields(first_field.field_info.annotation)975976 if isinstance(received_body, FormData):977 body_to_process = await _extract_form_body(fields_to_extract, received_body)978979 if single_not_embedded_field:980 loc: tuple[str, ...] = ("body",)981 v_, errors_ = _validate_value_with_model_field(982 field=first_field, value=body_to_process, values=values, loc=loc983 )984 return {first_field.name: v_}, errors_985 for field in body_fields:986 loc = ("body", get_validation_alias(field))987 value: Any | None = None988 if body_to_process is not None and not isinstance(body_to_process, bytes):989 try:990 value = body_to_process.get(get_validation_alias(field))991 # If the received body is a list, not a dict992 except AttributeError:993 errors.append(get_missing_field_error(loc))994 continue995 v_, errors_ = _validate_value_with_model_field(996 field=field, value=value, values=values, loc=loc997 )998 if errors_:999 errors.extend(errors_)1000 else:1001 values[field.name] = v_1002 return values, errors100310041005def get_body_field(1006 *, flat_dependant: Dependant, name: str, embed_body_fields: bool1007) -> ModelField | None:1008 """1009 Get a ModelField representing the request body for a path operation, combining1010 all body parameters into a single field if necessary.10111012 Used to check if it's form data (with `isinstance(body_field, params.Form)`)1013 or JSON and to generate the JSON Schema for a request body.10141015 This is **not** used to validate/parse the request body, that's done with each1016 individual body parameter.1017 """1018 if not flat_dependant.body_params:1019 return None1020 first_param = flat_dependant.body_params[0]1021 if not embed_body_fields:1022 return first_param1023 model_name = "Body_" + name1024 BodyModel = create_body_model(1025 fields=flat_dependant.body_params, model_name=model_name1026 )1027 required = any(1028 True for f in flat_dependant.body_params if f.field_info.is_required()1029 )1030 BodyFieldInfo_kwargs: dict[str, Any] = {1031 "annotation": BodyModel,1032 "alias": "body",1033 }1034 if not required:1035 BodyFieldInfo_kwargs["default"] = None1036 if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params):1037 BodyFieldInfo: type[params.Body] = params.File1038 elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params):1039 BodyFieldInfo = params.Form1040 else:1041 BodyFieldInfo = params.Body10421043 body_param_media_types = [1044 f.field_info.media_type1045 for f in flat_dependant.body_params1046 if isinstance(f.field_info, params.Body)1047 ]1048 if len(set(body_param_media_types)) == 1:1049 BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0]1050 final_field = create_model_field(1051 name="body",1052 type_=BodyModel,1053 alias="body",1054 field_info=BodyFieldInfo(**BodyFieldInfo_kwargs),1055 )1056 return final_field105710581059def get_validation_alias(field: ModelField) -> str:1060 va = getattr(field, "validation_alias", None)1061 return va or field.alias
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.