Browse Source

♻️ Refactor internals to not use Pydantic v1

pull/14609/head
Sebastián Ramírez 7 months ago
parent
commit
aa11eb51a2
  1. 15
      fastapi/datastructures.py
  2. 84
      fastapi/dependencies/utils.py
  3. 25
      fastapi/encoders.py
  4. 22
      fastapi/openapi/models.py
  5. 7
      fastapi/openapi/utils.py
  6. 50
      fastapi/routing.py
  7. 109
      fastapi/utils.py

15
fastapi/datastructures.py

@ -1,3 +1,4 @@
from collections.abc import Mapping
from typing import ( from typing import (
Annotated, Annotated,
Any, Any,
@ -9,11 +10,7 @@ from typing import (
) )
from annotated_doc import Doc from annotated_doc import Doc
from fastapi._compat import ( from pydantic import GetJsonSchemaHandler
CoreSchema,
GetJsonSchemaHandler,
JsonSchemaValue,
)
from starlette.datastructures import URL as URL # noqa: F401 from starlette.datastructures import URL as URL # noqa: F401
from starlette.datastructures import Address as Address # noqa: F401 from starlette.datastructures import Address as Address # noqa: F401
from starlette.datastructures import FormData as FormData # noqa: F401 from starlette.datastructures import FormData as FormData # noqa: F401
@ -142,14 +139,14 @@ class UploadFile(StarletteUploadFile):
@classmethod @classmethod
def __get_pydantic_json_schema__( def __get_pydantic_json_schema__(
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler cls, core_schema: Mapping[str, Any], handler: GetJsonSchemaHandler
) -> JsonSchemaValue: ) -> dict[str, Any]:
return {"type": "string", "format": "binary"} return {"type": "string", "format": "binary"}
@classmethod @classmethod
def __get_pydantic_core_schema__( def __get_pydantic_core_schema__(
cls, source: type[Any], handler: Callable[[Any], CoreSchema] cls, source: type[Any], handler: Callable[[Any], Mapping[str, Any]]
) -> CoreSchema: ) -> Mapping[str, Any]:
from ._compat.v2 import with_info_plain_validator_function from ._compat.v2 import with_info_plain_validator_function
return with_info_plain_validator_function(cls._validate) return with_info_plain_validator_function(cls._validate)

84
fastapi/dependencies/utils.py

@ -21,7 +21,6 @@ from fastapi._compat import (
ModelField, ModelField,
RequiredParam, RequiredParam,
Undefined, Undefined,
_is_error_wrapper,
_is_model_class, _is_model_class,
copy_field_info, copy_field_info,
create_body_model, create_body_model,
@ -43,7 +42,6 @@ from fastapi._compat import (
serialize_sequence_value, serialize_sequence_value,
value_is_sequence, value_is_sequence,
) )
from fastapi._compat.shared import annotation_is_pydantic_v1
from fastapi.background import BackgroundTasks from fastapi.background import BackgroundTasks
from fastapi.concurrency import ( from fastapi.concurrency import (
asynccontextmanager, asynccontextmanager,
@ -71,8 +69,6 @@ from starlette.responses import Response
from starlette.websockets import WebSocket from starlette.websockets import WebSocket
from typing_extensions import Literal, get_args, get_origin from typing_extensions import Literal, get_args, get_origin
from .. import temp_pydantic_v1_params
multipart_not_installed_error = ( multipart_not_installed_error = (
'Form data requires "python-multipart" to be installed. \n' 'Form data requires "python-multipart" to be installed. \n'
'You can install "python-multipart" with: \n\n' 'You can install "python-multipart" with: \n\n'
@ -327,9 +323,7 @@ def get_dependant(
"pydantic.v1 models are no longer supported by FastAPI." "pydantic.v1 models are no longer supported by FastAPI."
f" Please update the param {param_name}: {param_details.type_annotation!r}." f" Please update the param {param_name}: {param_details.type_annotation!r}."
) )
if isinstance( if isinstance(param_details.field.field_info, params.Body):
param_details.field.field_info, (params.Body, temp_pydantic_v1_params.Body)
):
dependant.body_params.append(param_details.field) dependant.body_params.append(param_details.field)
else: else:
add_param_to_fields(field=param_details.field, dependant=dependant) add_param_to_fields(field=param_details.field, dependant=dependant)
@ -388,7 +382,7 @@ def analyze_param(
fastapi_annotations = [ fastapi_annotations = [
arg arg
for arg in annotated_args[1:] for arg in annotated_args[1:]
if isinstance(arg, (FieldInfo, may_v1.FieldInfo, params.Depends)) if isinstance(arg, (FieldInfo, params.Depends))
] ]
fastapi_specific_annotations = [ fastapi_specific_annotations = [
arg arg
@ -397,30 +391,27 @@ def analyze_param(
arg, arg,
( (
params.Param, params.Param,
temp_pydantic_v1_params.Param,
params.Body, params.Body,
temp_pydantic_v1_params.Body,
params.Depends, params.Depends,
), ),
) )
] ]
if fastapi_specific_annotations: if fastapi_specific_annotations:
fastapi_annotation: Union[ fastapi_annotation: Union[FieldInfo, params.Depends, None] = (
FieldInfo, may_v1.FieldInfo, params.Depends, None fastapi_specific_annotations[-1]
] = fastapi_specific_annotations[-1] )
else: else:
fastapi_annotation = None fastapi_annotation = None
# Set default for Annotated FieldInfo # Set default for Annotated FieldInfo
if isinstance(fastapi_annotation, (FieldInfo, may_v1.FieldInfo)): if isinstance(fastapi_annotation, FieldInfo):
# Copy `field_info` because we mutate `field_info.default` below. # Copy `field_info` because we mutate `field_info.default` below.
field_info = copy_field_info( field_info = copy_field_info(
field_info=fastapi_annotation, # type: ignore[arg-type] field_info=fastapi_annotation, # type: ignore[arg-type]
annotation=use_annotation, annotation=use_annotation,
) )
assert field_info.default in { assert (
Undefined, field_info.default == Undefined or field_info.default == RequiredParam
may_v1.Undefined, ), (
} or field_info.default in {RequiredParam, may_v1.RequiredParam}, (
f"`{field_info.__class__.__name__}` default value cannot be set in" f"`{field_info.__class__.__name__}` default value cannot be set in"
f" `Annotated` for {param_name!r}. Set the default value with `=` instead." f" `Annotated` for {param_name!r}. Set the default value with `=` instead."
) )
@ -444,7 +435,7 @@ def analyze_param(
) )
depends = value depends = value
# Get FieldInfo from default value # Get FieldInfo from default value
elif isinstance(value, (FieldInfo, may_v1.FieldInfo)): elif isinstance(value, FieldInfo):
assert field_info is None, ( assert field_info is None, (
"Cannot specify FastAPI annotations in `Annotated` and default value" "Cannot specify FastAPI annotations in `Annotated` and default value"
f" together for {param_name!r}" f" together for {param_name!r}"
@ -488,14 +479,7 @@ def analyze_param(
) or is_uploadfile_sequence_annotation(type_annotation): ) or is_uploadfile_sequence_annotation(type_annotation):
field_info = params.File(annotation=use_annotation, default=default_value) field_info = params.File(annotation=use_annotation, default=default_value)
elif not field_annotation_is_scalar(annotation=type_annotation): elif not field_annotation_is_scalar(annotation=type_annotation):
if annotation_is_pydantic_v1(use_annotation): field_info = params.Body(annotation=use_annotation, default=default_value)
field_info = temp_pydantic_v1_params.Body( # type: ignore[assignment]
annotation=use_annotation, default=default_value
)
else:
field_info = params.Body(
annotation=use_annotation, default=default_value
)
else: else:
field_info = params.Query(annotation=use_annotation, default=default_value) field_info = params.Query(annotation=use_annotation, default=default_value)
@ -504,14 +488,12 @@ def analyze_param(
if field_info is not None: if field_info is not None:
# Handle field_info.in_ # Handle field_info.in_
if is_path_param: if is_path_param:
assert isinstance( assert isinstance(field_info, params.Path), (
field_info, (params.Path, temp_pydantic_v1_params.Path)
), (
f"Cannot use `{field_info.__class__.__name__}` for path param" f"Cannot use `{field_info.__class__.__name__}` for path param"
f" {param_name!r}" f" {param_name!r}"
) )
elif ( elif (
isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)) isinstance(field_info, params.Param)
and getattr(field_info, "in_", None) is None and getattr(field_info, "in_", None) is None
): ):
field_info.in_ = params.ParamTypes.query field_info.in_ = params.ParamTypes.query
@ -520,7 +502,7 @@ def analyze_param(
field_info, field_info,
param_name, param_name,
) )
if isinstance(field_info, (params.Form, temp_pydantic_v1_params.Form)): if isinstance(field_info, params.Form):
ensure_multipart_is_installed() ensure_multipart_is_installed()
if not field_info.alias and getattr(field_info, "convert_underscores", None): if not field_info.alias and getattr(field_info, "convert_underscores", None):
alias = param_name.replace("_", "-") alias = param_name.replace("_", "-")
@ -532,15 +514,14 @@ def analyze_param(
type_=use_annotation_from_field_info, type_=use_annotation_from_field_info,
default=field_info.default, default=field_info.default,
alias=alias, alias=alias,
required=field_info.default required=field_info.default in (RequiredParam, Undefined),
in (RequiredParam, may_v1.RequiredParam, Undefined),
field_info=field_info, field_info=field_info,
) )
if is_path_param: if is_path_param:
assert is_scalar_field(field=field), ( assert is_scalar_field(field=field), (
"Path params must be of one of the supported types" "Path params must be of one of the supported types"
) )
elif isinstance(field_info, (params.Query, temp_pydantic_v1_params.Query)): elif isinstance(field_info, params.Query):
assert ( assert (
is_scalar_field(field) is_scalar_field(field)
or is_scalar_sequence_field(field) or is_scalar_sequence_field(field)
@ -739,9 +720,7 @@ def _validate_value_with_model_field(
else: else:
return deepcopy(field.default), [] return deepcopy(field.default), []
v_, errors_ = field.validate(value, values, loc=loc) v_, errors_ = field.validate(value, values, loc=loc)
if _is_error_wrapper(errors_): # type: ignore[arg-type] if isinstance(errors_, list):
return None, [errors_]
elif isinstance(errors_, list):
new_errors = may_v1._regenerate_error_with_loc(errors=errors_, loc_prefix=()) new_errors = may_v1._regenerate_error_with_loc(errors=errors_, loc_prefix=())
return None, new_errors return None, new_errors
else: else:
@ -759,7 +738,7 @@ def _get_multidict_value(
if ( if (
value is None value is None
or ( or (
isinstance(field.field_info, (params.Form, temp_pydantic_v1_params.Form)) isinstance(field.field_info, params.Form)
and isinstance(value, str) # For type checks and isinstance(value, str) # For type checks
and value == "" and value == ""
) )
@ -829,7 +808,7 @@ def request_params_to_args(
if single_not_embedded_field: if single_not_embedded_field:
field_info = first_field.field_info field_info = first_field.field_info
assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), ( assert isinstance(field_info, params.Param), (
"Params must be subclasses of Param" "Params must be subclasses of Param"
) )
loc: tuple[str, ...] = (field_info.in_.value,) loc: tuple[str, ...] = (field_info.in_.value,)
@ -841,7 +820,7 @@ def request_params_to_args(
for field in fields: for field in fields:
value = _get_multidict_value(field, received_params) value = _get_multidict_value(field, received_params)
field_info = field.field_info field_info = field.field_info
assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), ( assert isinstance(field_info, params.Param), (
"Params must be subclasses of Param" "Params must be subclasses of Param"
) )
loc = (field_info.in_.value, get_validation_alias(field)) loc = (field_info.in_.value, get_validation_alias(field))
@ -890,7 +869,7 @@ def _should_embed_body_fields(fields: list[ModelField]) -> bool:
# If it's a Form (or File) field, it has to be a BaseModel (or a union of BaseModels) to be top level # If it's a Form (or File) field, it has to be a BaseModel (or a union of BaseModels) to be top level
# otherwise it has to be embedded, so that the key value pair can be extracted # otherwise it has to be embedded, so that the key value pair can be extracted
if ( if (
isinstance(first_field.field_info, (params.Form, temp_pydantic_v1_params.Form)) isinstance(first_field.field_info, params.Form)
and not _is_model_class(first_field.type_) and not _is_model_class(first_field.type_)
and not is_union_of_base_models(first_field.type_) and not is_union_of_base_models(first_field.type_)
): ):
@ -908,14 +887,14 @@ async def _extract_form_body(
value = _get_multidict_value(field, received_body) value = _get_multidict_value(field, received_body)
field_info = field.field_info field_info = field.field_info
if ( if (
isinstance(field_info, (params.File, temp_pydantic_v1_params.File)) isinstance(field_info, params.File)
and is_bytes_field(field) and is_bytes_field(field)
and isinstance(value, UploadFile) and isinstance(value, UploadFile)
): ):
value = await value.read() value = await value.read()
elif ( elif (
is_bytes_sequence_field(field) is_bytes_sequence_field(field)
and isinstance(field_info, (params.File, temp_pydantic_v1_params.File)) and isinstance(field_info, params.File)
and value_is_sequence(value) and value_is_sequence(value)
): ):
# For types # For types
@ -1026,28 +1005,15 @@ def get_body_field(
BodyFieldInfo_kwargs["default"] = None 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 flat_dependant.body_params):
BodyFieldInfo: type[params.Body] = params.File BodyFieldInfo: type[params.Body] = params.File
elif any(
isinstance(f.field_info, temp_pydantic_v1_params.File)
for f in flat_dependant.body_params
):
BodyFieldInfo: type[temp_pydantic_v1_params.Body] = temp_pydantic_v1_params.File # type: ignore[no-redef]
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 flat_dependant.body_params):
BodyFieldInfo = params.Form BodyFieldInfo = params.Form
elif any(
isinstance(f.field_info, temp_pydantic_v1_params.Form)
for f in flat_dependant.body_params
):
BodyFieldInfo = temp_pydantic_v1_params.Form # type: ignore[assignment]
else: else:
if annotation_is_pydantic_v1(BodyModel): BodyFieldInfo = params.Body
BodyFieldInfo = temp_pydantic_v1_params.Body # type: ignore[assignment]
else:
BodyFieldInfo = params.Body
body_param_media_types = [ body_param_media_types = [
f.field_info.media_type f.field_info.media_type
for f in flat_dependant.body_params for f in flat_dependant.body_params
if isinstance(f.field_info, (params.Body, temp_pydantic_v1_params.Body)) if isinstance(f.field_info, params.Body)
] ]
if len(set(body_param_media_types)) == 1: if len(set(body_param_media_types)) == 1:
BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0] BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0]

25
fastapi/encoders.py

@ -18,14 +18,14 @@ from typing import Annotated, Any, Callable, Optional, Union
from uuid import UUID from uuid import UUID
from annotated_doc import Doc from annotated_doc import Doc
from fastapi._compat import may_v1 from fastapi.exceptions import PydanticV1NotSupportedError
from fastapi.types import IncEx from fastapi.types import IncEx
from pydantic import BaseModel from pydantic import BaseModel
from pydantic.color import Color from pydantic.color import Color
from pydantic.networks import AnyUrl, NameEmail from pydantic.networks import AnyUrl, NameEmail
from pydantic.types import SecretBytes, SecretStr from pydantic.types import SecretBytes, SecretStr
from ._compat import Url, _is_undefined, _model_dump from ._compat import Url, _is_undefined, _model_dump, may_v1
# Taken from Pydantic v1 as is # Taken from Pydantic v1 as is
@ -63,7 +63,6 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
ENCODERS_BY_TYPE: dict[type[Any], Callable[[Any], Any]] = { ENCODERS_BY_TYPE: dict[type[Any], Callable[[Any], Any]] = {
bytes: lambda o: o.decode(), bytes: lambda o: o.decode(),
Color: str, Color: str,
may_v1.Color: str,
datetime.date: isoformat, datetime.date: isoformat,
datetime.datetime: isoformat, datetime.datetime: isoformat,
datetime.time: isoformat, datetime.time: isoformat,
@ -80,19 +79,14 @@ ENCODERS_BY_TYPE: dict[type[Any], Callable[[Any], Any]] = {
IPv6Interface: str, IPv6Interface: str,
IPv6Network: str, IPv6Network: str,
NameEmail: str, NameEmail: str,
may_v1.NameEmail: str,
Path: str, Path: str,
Pattern: lambda o: o.pattern, Pattern: lambda o: o.pattern,
SecretBytes: str, SecretBytes: str,
may_v1.SecretBytes: str,
SecretStr: str, SecretStr: str,
may_v1.SecretStr: str,
set: list, set: list,
UUID: str, UUID: str,
Url: str, Url: str,
may_v1.Url: str,
AnyUrl: str, AnyUrl: str,
may_v1.AnyUrl: str,
} }
@ -224,13 +218,12 @@ def jsonable_encoder(
include = set(include) include = set(include)
if exclude is not None and not isinstance(exclude, (set, dict)): if exclude is not None and not isinstance(exclude, (set, dict)):
exclude = set(exclude) exclude = set(exclude)
if isinstance(obj, (BaseModel, may_v1.BaseModel)): if isinstance(obj, may_v1.BaseModel):
# TODO: remove when deprecating Pydantic v1 raise PydanticV1NotSupportedError(
encoders: dict[Any, Any] = {} "pydantic.v1 models are no longer supported by FastAPI."
if isinstance(obj, may_v1.BaseModel): f" Please update the model {obj!r}."
encoders = getattr(obj.__config__, "json_encoders", {}) )
if custom_encoder: if isinstance(obj, BaseModel):
encoders = {**encoders, **custom_encoder}
obj_dict = _model_dump( obj_dict = _model_dump(
obj, # type: ignore[arg-type] obj, # type: ignore[arg-type]
mode="json", mode="json",
@ -247,8 +240,6 @@ def jsonable_encoder(
obj_dict, obj_dict,
exclude_none=exclude_none, exclude_none=exclude_none,
exclude_defaults=exclude_defaults, exclude_defaults=exclude_defaults,
# TODO: remove when deprecating Pydantic v1
custom_encoder=encoders,
sqlalchemy_safe=sqlalchemy_safe, sqlalchemy_safe=sqlalchemy_safe,
) )
if dataclasses.is_dataclass(obj): if dataclasses.is_dataclass(obj):

22
fastapi/openapi/models.py

@ -1,15 +1,15 @@
from collections.abc import Iterable from collections.abc import Iterable, Mapping
from enum import Enum from enum import Enum
from typing import Annotated, Any, Callable, Optional, Union from typing import Annotated, Any, Callable, Optional, Union
from fastapi._compat import ( from fastapi._compat import with_info_plain_validator_function
CoreSchema, from fastapi.logger import logger
from pydantic import (
AnyUrl,
BaseModel,
Field,
GetJsonSchemaHandler, GetJsonSchemaHandler,
JsonSchemaValue,
with_info_plain_validator_function,
) )
from fastapi.logger import logger
from pydantic import AnyUrl, BaseModel, Field
from typing_extensions import Literal, TypedDict from typing_extensions import Literal, TypedDict
from typing_extensions import deprecated as typing_deprecated from typing_extensions import deprecated as typing_deprecated
@ -43,14 +43,14 @@ except ImportError: # pragma: no cover
@classmethod @classmethod
def __get_pydantic_json_schema__( def __get_pydantic_json_schema__(
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler cls, core_schema: Mapping[str, Any], handler: GetJsonSchemaHandler
) -> JsonSchemaValue: ) -> dict[str, Any]:
return {"type": "string", "format": "email"} return {"type": "string", "format": "email"}
@classmethod @classmethod
def __get_pydantic_core_schema__( def __get_pydantic_core_schema__(
cls, source: type[Any], handler: Callable[[Any], CoreSchema] cls, source: type[Any], handler: Callable[[Any], Mapping[str, Any]]
) -> CoreSchema: ) -> Mapping[str, Any]:
return with_info_plain_validator_function(cls._validate) return with_info_plain_validator_function(cls._validate)

7
fastapi/openapi/utils.py

@ -6,7 +6,6 @@ from typing import Any, Optional, Union, cast
from fastapi import routing from fastapi import routing
from fastapi._compat import ( from fastapi._compat import (
JsonSchemaValue,
ModelField, ModelField,
Undefined, Undefined,
get_compat_model_name_map, get_compat_model_name_map,
@ -109,7 +108,7 @@ def _get_openapi_operation_parameters(
dependant: Dependant, dependant: Dependant,
model_name_map: ModelNameMap, model_name_map: ModelNameMap,
field_mapping: dict[ field_mapping: dict[
tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any]
], ],
separate_input_output_schemas: bool = True, separate_input_output_schemas: bool = True,
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
@ -182,7 +181,7 @@ def get_openapi_operation_request_body(
body_field: Optional[ModelField], body_field: Optional[ModelField],
model_name_map: ModelNameMap, model_name_map: ModelNameMap,
field_mapping: dict[ field_mapping: dict[
tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any]
], ],
separate_input_output_schemas: bool = True, separate_input_output_schemas: bool = True,
) -> Optional[dict[str, Any]]: ) -> Optional[dict[str, Any]]:
@ -265,7 +264,7 @@ def get_openapi_path(
operation_ids: set[str], operation_ids: set[str],
model_name_map: ModelNameMap, model_name_map: ModelNameMap,
field_mapping: dict[ field_mapping: dict[
tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any]
], ],
separate_input_output_schemas: bool = True, separate_input_output_schemas: bool = True,
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]: ) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:

50
fastapi/routing.py

@ -21,16 +21,13 @@ from typing import (
) )
from annotated_doc import Doc from annotated_doc import Doc
from fastapi import params, temp_pydantic_v1_params from fastapi import params
from fastapi._compat import ( from fastapi._compat import (
ModelField, ModelField,
Undefined, Undefined,
_get_model_config,
_model_dump,
_normalize_errors, _normalize_errors,
annotation_is_pydantic_v1, annotation_is_pydantic_v1,
lenient_issubclass, lenient_issubclass,
may_v1,
) )
from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.datastructures import Default, DefaultPlaceholder
from fastapi.dependencies.models import Dependant from fastapi.dependencies.models import Dependant
@ -154,22 +151,7 @@ def _prepare_response_content(
exclude_defaults: bool = False, exclude_defaults: bool = False,
exclude_none: bool = False, exclude_none: bool = False,
) -> Any: ) -> Any:
if isinstance(res, may_v1.BaseModel): if isinstance(res, list):
read_with_orm_mode = getattr(_get_model_config(res), "read_with_orm_mode", None) # type: ignore[arg-type]
if read_with_orm_mode:
# Let from_orm extract the data from this model instead of converting
# it now to a dict.
# Otherwise, there's no way to extract lazy data that requires attribute
# access instead of dict iteration, e.g. lazy relationships.
return res
return _model_dump(
res, # type: ignore[arg-type]
by_alias=True,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
elif isinstance(res, list):
return [ return [
_prepare_response_content( _prepare_response_content(
item, item,
@ -251,14 +233,6 @@ async def serialize_response(
) -> Any: ) -> Any:
if field: if field:
errors = [] errors = []
if not hasattr(field, "serialize"):
# pydantic v1
response_content = _prepare_response_content(
response_content,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
if is_coroutine: if is_coroutine:
value, errors_ = field.validate(response_content, {}, loc=("response",)) value, errors_ = field.validate(response_content, {}, loc=("response",))
else: else:
@ -267,8 +241,6 @@ async def serialize_response(
) )
if isinstance(errors_, list): if isinstance(errors_, list):
errors.extend(errors_) errors.extend(errors_)
elif errors_:
errors.append(errors_)
if errors: if errors:
ctx = endpoint_ctx or EndpointContext() ctx = endpoint_ctx or EndpointContext()
raise ResponseValidationError( raise ResponseValidationError(
@ -277,18 +249,7 @@ async def serialize_response(
endpoint_ctx=ctx, endpoint_ctx=ctx,
) )
if hasattr(field, "serialize"): return field.serialize(
return field.serialize(
value,
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
return jsonable_encoder(
value, value,
include=include, include=include,
exclude=exclude, exclude=exclude,
@ -297,6 +258,7 @@ async def serialize_response(
exclude_defaults=exclude_defaults, exclude_defaults=exclude_defaults,
exclude_none=exclude_none, exclude_none=exclude_none,
) )
else: else:
return jsonable_encoder(response_content) return jsonable_encoder(response_content)
@ -331,9 +293,7 @@ def get_request_handler(
) -> Callable[[Request], Coroutine[Any, Any, Response]]: ) -> Callable[[Request], Coroutine[Any, Any, Response]]:
assert dependant.call is not None, "dependant.call must be a function" assert dependant.call is not None, "dependant.call must be a function"
is_coroutine = dependant.is_coroutine_callable is_coroutine = dependant.is_coroutine_callable
is_body_form = body_field and isinstance( is_body_form = body_field and isinstance(body_field.field_info, params.Form)
body_field.field_info, (params.Form, temp_pydantic_v1_params.Form)
)
if isinstance(response_class, DefaultPlaceholder): if isinstance(response_class, DefaultPlaceholder):
actual_response_class: type[Response] = response_class.value actual_response_class: type[Response] = response_class.value
else: else:

109
fastapi/utils.py

@ -6,7 +6,6 @@ from typing import (
Any, Any,
Optional, Optional,
Union, Union,
cast,
) )
from weakref import WeakKeyDictionary from weakref import WeakKeyDictionary
@ -19,11 +18,9 @@ from fastapi._compat import (
UndefinedType, UndefinedType,
Validator, Validator,
annotation_is_pydantic_v1, annotation_is_pydantic_v1,
lenient_issubclass,
may_v1,
) )
from fastapi.datastructures import DefaultPlaceholder, DefaultType from fastapi.datastructures import DefaultPlaceholder, DefaultType
from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.exceptions import FastAPIDeprecationWarning, PydanticV1NotSupportedError
from pydantic import BaseModel from pydantic import BaseModel
from pydantic.fields import FieldInfo from pydantic.fields import FieldInfo
from typing_extensions import Literal from typing_extensions import Literal
@ -83,52 +80,18 @@ def create_model_field(
mode: Literal["validation", "serialization"] = "validation", mode: Literal["validation", "serialization"] = "validation",
version: Literal["1", "auto"] = "auto", version: Literal["1", "auto"] = "auto",
) -> ModelField: ) -> ModelField:
class_validators = class_validators or {} if annotation_is_pydantic_v1(type_):
raise PydanticV1NotSupportedError(
v1_model_config = may_v1.BaseConfig "pydantic.v1 models are no longer supported by FastAPI."
v1_field_info = field_info or may_v1.FieldInfo() f" Please update the response model {type_!r}."
v1_kwargs = {
"name": name,
"field_info": v1_field_info,
"type_": type_,
"class_validators": class_validators,
"default": default,
"required": required,
"model_config": v1_model_config,
"alias": alias,
}
if (
annotation_is_pydantic_v1(type_)
or isinstance(field_info, may_v1.FieldInfo)
or version == "1"
):
from fastapi._compat import v1
try:
return v1.ModelField(**v1_kwargs) # type: ignore[return-value]
except RuntimeError:
raise fastapi.exceptions.FastAPIError(
_invalid_args_message.format(type_=type_)
) from None
else:
field_info = field_info or FieldInfo(
annotation=type_, default=default, alias=alias
) )
kwargs = {"mode": mode, "name": name, "field_info": field_info} class_validators = class_validators or {}
try:
return v2.ModelField(**kwargs) # type: ignore[return-value,arg-type]
except PydanticSchemaGenerationError:
raise fastapi.exceptions.FastAPIError(
_invalid_args_message.format(type_=type_)
) from None
# Pydantic v2 is not installed, but it's not a Pydantic v1 ModelField, it could be
# a Pydantic v1 type, like a constrained int
from fastapi._compat import v1
field_info = field_info or FieldInfo(annotation=type_, default=default, alias=alias)
kwargs = {"mode": mode, "name": name, "field_info": field_info}
try: try:
return v1.ModelField(**v1_kwargs) return v2.ModelField(**kwargs) # type: ignore[return-value,arg-type]
except RuntimeError: except PydanticSchemaGenerationError:
raise fastapi.exceptions.FastAPIError( raise fastapi.exceptions.FastAPIError(
_invalid_args_message.format(type_=type_) _invalid_args_message.format(type_=type_)
) from None ) from None
@ -139,57 +102,7 @@ def create_cloned_field(
*, *,
cloned_types: Optional[MutableMapping[type[BaseModel], type[BaseModel]]] = None, cloned_types: Optional[MutableMapping[type[BaseModel], type[BaseModel]]] = None,
) -> ModelField: ) -> ModelField:
if isinstance(field, v2.ModelField): return field
return field
from fastapi._compat import v1
# cloned_types caches already cloned types to support recursive models and improve
# performance by avoiding unnecessary cloning
if cloned_types is None:
cloned_types = _CLONED_TYPES_CACHE
original_type = field.type_
use_type = original_type
if lenient_issubclass(original_type, v1.BaseModel):
original_type = cast(type[v1.BaseModel], original_type)
use_type = cloned_types.get(original_type)
if use_type is None:
use_type = v1.create_model(original_type.__name__, __base__=original_type)
cloned_types[original_type] = use_type
for f in original_type.__fields__.values():
use_type.__fields__[f.name] = create_cloned_field(
f,
cloned_types=cloned_types,
)
new_field = create_model_field(name=field.name, type_=use_type, version="1")
new_field.has_alias = field.has_alias # type: ignore[attr-defined]
new_field.alias = field.alias # type: ignore[misc]
new_field.class_validators = field.class_validators # type: ignore[attr-defined]
new_field.default = field.default # type: ignore[misc]
new_field.default_factory = field.default_factory # type: ignore[attr-defined]
new_field.required = field.required # type: ignore[misc]
new_field.model_config = field.model_config # type: ignore[attr-defined]
new_field.field_info = field.field_info
new_field.allow_none = field.allow_none # type: ignore[attr-defined]
new_field.validate_always = field.validate_always # type: ignore[attr-defined]
if field.sub_fields: # type: ignore[attr-defined]
new_field.sub_fields = [ # type: ignore[attr-defined]
create_cloned_field(sub_field, cloned_types=cloned_types)
for sub_field in field.sub_fields # type: ignore[attr-defined]
]
if field.key_field: # type: ignore[attr-defined]
new_field.key_field = create_cloned_field( # type: ignore[attr-defined]
field.key_field, # type: ignore[attr-defined]
cloned_types=cloned_types,
)
new_field.validators = field.validators # type: ignore[attr-defined]
new_field.pre_validators = field.pre_validators # type: ignore[attr-defined]
new_field.post_validators = field.post_validators # type: ignore[attr-defined]
new_field.parse_json = field.parse_json # type: ignore[attr-defined]
new_field.shape = field.shape # type: ignore[attr-defined]
new_field.populate_validators() # type: ignore[attr-defined]
return new_field
def generate_operation_id_for_path( def generate_operation_id_for_path(

Loading…
Cancel
Save