Browse Source

📝 Fix parameter handling to support flat fields extraction

pull/15130/head
Maksim Zayats 4 months ago
parent
commit
7c853ca87a
  1. 95
      fastapi/dependencies/utils.py
  2. 64
      fastapi/openapi/utils.py

95
fastapi/dependencies/utils.py

@ -189,16 +189,32 @@ def get_flat_dependant(
return flat_dependant
@dataclass
class _FlatParamField:
field: ModelField
original_field: ModelField
def _is_model_param(field: ModelField) -> bool:
return lenient_issubclass(field.field_info.annotation, BaseModel)
def _get_flat_param_fields(fields: Sequence[ModelField]) -> list[_FlatParamField]:
flat_fields: list[_FlatParamField] = []
for field in fields:
if _is_model_param(field):
annotation = cast(type[BaseModel], field.field_info.annotation)
flat_fields.extend(
_FlatParamField(field=sub_field, original_field=field)
for sub_field in get_cached_model_fields(annotation)
)
else:
flat_fields.append(_FlatParamField(field=field, original_field=field))
return flat_fields
def _get_flat_fields_from_params(fields: list[ModelField]) -> list[ModelField]:
if not fields:
return fields
first_field = fields[0]
if len(fields) == 1 and lenient_issubclass(
first_field.field_info.annotation, BaseModel
):
fields_to_extract = get_cached_model_fields(first_field.field_info.annotation)
return fields_to_extract
return fields
return [flat_field.field for flat_field in _get_flat_param_fields(fields)]
def get_flat_params(dependant: Dependant) -> list[ModelField]:
@ -791,32 +807,25 @@ def request_params_to_args(
if not fields:
return values, errors
first_field = fields[0]
fields_to_extract = fields
single_not_embedded_field = False
default_convert_underscores = True
if len(fields) == 1 and lenient_issubclass(
first_field.field_info.annotation, BaseModel
):
fields_to_extract = get_cached_model_fields(first_field.field_info.annotation)
single_not_embedded_field = True
# If headers are in a Pydantic model, the way to disable convert_underscores
# would be with Header(convert_underscores=False) at the Pydantic model level
default_convert_underscores = getattr(
first_field.field_info, "convert_underscores", True
)
params_to_process: dict[str, Any] = {}
flat_fields = _get_flat_param_fields(fields)
model_fields = [field for field in fields if _is_model_param(field)]
processed_extras: dict[str, Any] = {}
params_by_field: dict[int, dict[str, Any]] = {
id(field): {} for field in model_fields
}
processed_keys = set()
for field in fields_to_extract:
for flat_field in flat_fields:
field = flat_field.field
original_field = flat_field.original_field
alias = None
if isinstance(received_params, Headers):
# Handle fields extracted from a Pydantic Model for a header, each field
# doesn't have a FieldInfo of type Header with the default convert_underscores=True
convert_underscores = getattr(
field.field_info, "convert_underscores", default_convert_underscores
field.field_info,
"convert_underscores",
getattr(original_field.field_info, "convert_underscores", True),
)
if convert_underscores:
alias = get_validation_alias(field)
@ -824,7 +833,8 @@ def request_params_to_args(
alias = alias.replace("_", "-")
value = _get_multidict_value(field, received_params, alias=alias)
if value is not None:
params_to_process[get_validation_alias(field)] = value
if _is_model_param(original_field):
params_by_field[id(original_field)][get_validation_alias(field)] = value
processed_keys.add(alias or get_validation_alias(field))
for key in received_params.keys():
@ -832,30 +842,27 @@ def request_params_to_args(
if isinstance(received_params, (ImmutableMultiDict, Headers)):
value = received_params.getlist(key)
if isinstance(value, list) and (len(value) == 1):
params_to_process[key] = value[0]
processed_extras[key] = value[0]
else:
params_to_process[key] = value
processed_extras[key] = value
else:
params_to_process[key] = received_params.get(key)
processed_extras[key] = received_params.get(key)
if single_not_embedded_field:
field_info = first_field.field_info
assert isinstance(field_info, params.Param), (
"Params must be subclasses of Param"
)
loc: tuple[str, ...] = (field_info.in_.value,)
v_, errors_ = _validate_value_with_model_field(
field=first_field, value=params_to_process, values=values, loc=loc
)
return {first_field.name: v_}, errors_
if len(model_fields) == 1:
params_by_field[id(model_fields[0])].update(processed_extras)
for field in fields:
value = _get_multidict_value(field, received_params)
field_info = field.field_info
assert isinstance(field_info, params.Param), (
"Params must be subclasses of Param"
)
loc = (field_info.in_.value, get_validation_alias(field))
loc: tuple[str, ...]
if _is_model_param(field):
loc = (field_info.in_.value,)
value = params_by_field[id(field)]
else:
loc = (field_info.in_.value, get_validation_alias(field))
value = _get_multidict_value(field, received_params)
v_, errors_ = _validate_value_with_model_field(
field=field, value=value, values=values, loc=loc
)

64
fastapi/openapi/utils.py

@ -17,7 +17,9 @@ from fastapi._compat import (
from fastapi.datastructures import DefaultPlaceholder, _Unset
from fastapi.dependencies.models import Dependant
from fastapi.dependencies.utils import (
_FlatParamField,
_get_flat_fields_from_params,
_get_flat_param_fields,
get_flat_dependant,
get_flat_params,
get_validation_alias,
@ -35,7 +37,6 @@ from fastapi.utils import (
generate_operation_id_for_path,
is_body_allowed_for_status_code,
)
from pydantic import BaseModel
from starlette.responses import JSONResponse
from starlette.routing import BaseRoute
@ -117,21 +118,13 @@ def _get_openapi_operation_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)
header_params = _get_flat_param_fields(flat_dependant.header_params)
cookie_params = _get_flat_fields_from_params(flat_dependant.cookie_params)
parameter_groups = [
(ParamTypes.path, path_params),
(ParamTypes.query, query_params),
(ParamTypes.header, header_params),
(ParamTypes.cookie, cookie_params),
]
default_convert_underscores = True
if len(flat_dependant.header_params) == 1:
first_field = flat_dependant.header_params[0]
if lenient_issubclass(first_field.field_info.annotation, BaseModel):
default_convert_underscores = getattr(
first_field.field_info, "convert_underscores", True
)
for param_type, param_group in parameter_groups:
for param in param_group:
field_info = param.field_info
@ -145,17 +138,6 @@ def _get_openapi_operation_parameters(
separate_input_output_schemas=separate_input_output_schemas,
)
name = get_validation_alias(param)
convert_underscores = getattr(
param.field_info,
"convert_underscores",
default_convert_underscores,
)
if (
param_type == ParamTypes.header
and name == param.name
and convert_underscores
):
name = param.name.replace("_", "-")
parameter = {
"name": name,
@ -174,6 +156,46 @@ def _get_openapi_operation_parameters(
if getattr(field_info, "deprecated", None):
parameter["deprecated"] = True
parameters.append(parameter)
for header_param in header_params:
assert isinstance(header_param, _FlatParamField)
field_info = header_param.field.field_info
# field_info = cast(Param, field_info)
if not getattr(field_info, "include_in_schema", True):
continue
param_schema = get_schema_from_model_field(
field=header_param.field,
model_name_map=model_name_map,
field_mapping=field_mapping,
separate_input_output_schemas=separate_input_output_schemas,
)
name = get_validation_alias(header_param.field)
convert_underscores = getattr(
header_param.field.field_info,
"convert_underscores",
getattr(
header_param.original_field.field_info, "convert_underscores", True
),
)
if name == header_param.field.name and convert_underscores:
name = header_param.field.name.replace("_", "-")
parameter = {
"name": name,
"in": ParamTypes.header.value,
"required": header_param.field.field_info.is_required(),
"schema": param_schema,
}
if field_info.description:
parameter["description"] = field_info.description
openapi_examples = getattr(field_info, "openapi_examples", None)
example = getattr(field_info, "example", None)
if openapi_examples:
parameter["examples"] = jsonable_encoder(openapi_examples)
elif example is not _Unset:
parameter["example"] = jsonable_encoder(example)
if getattr(field_info, "deprecated", None):
parameter["deprecated"] = True
parameters.append(parameter)
return parameters

Loading…
Cancel
Save