Compare commits

..

19 Commits

Author SHA1 Message Date
Sebastián Ramírez
b32e65f5af ♻️ Handle Pydantic v1 imports for checks conditionally, filtering warnings 2025-12-26 22:01:36 +01:00
Sebastián Ramírez
fc50d9d438 🔄 Merge branch 'master' into drop-pv1 2025-12-26 21:41:12 +01:00
Sebastián Ramírez
4ef0128708 Add test for jsonable_encoder for coverage 2025-12-26 21:25:46 +01:00
Sebastián Ramírez
814d653192 🔧 Update pyproject.toml coverage files 2025-12-26 21:09:50 +01:00
Sebastián Ramírez
ea33214473 🔥 Remove Pydantic v1 example source files 2025-12-26 21:09:27 +01:00
Sebastián Ramírez
e5fa006f80 ♻️ Cleanup more functions and unused code 2025-12-26 21:09:02 +01:00
Sebastián Ramírez
ec213fa26e 🔥 Remove more unused references to Pydantic v1 2025-12-26 20:31:38 +01:00
Sebastián Ramírez
01289a46fc Restore and udpate test for coverage in jsonable_encoder 2025-12-26 20:28:28 +01:00
Sebastián Ramírez
0d6652bd32 🔧 Update config coverage for Pydantic v1 old test 2025-12-26 20:15:19 +01:00
Sebastián Ramírez
de015ae49d 🔥 Remove and update Pydantic v1 tests 2025-12-26 20:14:39 +01:00
Sebastián Ramírez
2e53a98830 🔥 Remove no longer needed test for Pydantic v1 2025-12-26 20:13:53 +01:00
Sebastián Ramírez
92ec1a08a0 🔥 Remove no longer supported Pydantic v1 field 2025-12-26 20:13:05 +01:00
Sebastián Ramírez
aa11eb51a2 ♻️ Refactor internals to not use Pydantic v1 2025-12-26 20:12:08 +01:00
Sebastián Ramírez
3c8db03107 ♻️ Refactor _compat, remove Pydantic v1, first pass 2025-12-26 20:09:36 +01:00
Sebastián Ramírez
5eacf7ee4c 🔄 Merge branch 'master' into drop-pv1 2025-12-26 17:27:28 +01:00
Sebastián Ramírez
7f76702908 🔥 Remove Pydantic v1 tests 2025-12-26 09:57:12 +01:00
Sebastián Ramírez
bde3f1ba9f 🚚 Rename file 2025-12-25 12:07:04 +01:00
Sebastián Ramírez
fd58f90369 Update tests for pydantic.v1 to ensure error 2025-12-25 12:06:27 +01:00
Sebastián Ramírez
4b0fca4cbd ♻️ Error out when using pydantic.v1 2025-12-25 12:05:51 +01:00
50 changed files with 235 additions and 8502 deletions

View File

@@ -1,20 +0,0 @@
from typing import Annotated
from fastapi import FastAPI
from fastapi.temp_pydantic_v1_params import Form
from pydantic.v1 import BaseModel
app = FastAPI()
class FormData(BaseModel):
username: str
password: str
class Config:
extra = "forbid"
@app.post("/login/")
async def login(data: Annotated[FormData, Form()]):
return data

View File

@@ -1,18 +0,0 @@
from fastapi import FastAPI
from fastapi.temp_pydantic_v1_params import Form
from pydantic.v1 import BaseModel
app = FastAPI()
class FormData(BaseModel):
username: str
password: str
class Config:
extra = "forbid"
@app.post("/login/")
async def login(data: FormData = Form()):
return data

View File

@@ -6,7 +6,6 @@ from .main import UndefinedType as UndefinedType
from .main import Url as Url
from .main import Validator as Validator
from .main import _get_model_config as _get_model_config
from .main import _is_error_wrapper as _is_error_wrapper
from .main import _is_model_class as _is_model_class
from .main import _is_model_field as _is_model_field
from .main import _is_undefined as _is_undefined
@@ -17,7 +16,6 @@ from .main import evaluate_forwardref as evaluate_forwardref
from .main import get_annotation_from_field_info as get_annotation_from_field_info
from .main import get_cached_model_fields as get_cached_model_fields
from .main import get_compat_model_name_map as get_compat_model_name_map
from .main import get_definitions as get_definitions
from .main import get_missing_field_error as get_missing_field_error
from .main import get_schema_from_model_field as get_schema_from_model_field
from .main import is_bytes_field as is_bytes_field
@@ -29,15 +27,12 @@ from .main import serialize_sequence_value as serialize_sequence_value
from .main import (
with_info_plain_validator_function as with_info_plain_validator_function,
)
from .may_v1 import CoreSchema as CoreSchema
from .may_v1 import GetJsonSchemaHandler as GetJsonSchemaHandler
from .may_v1 import JsonSchemaValue as JsonSchemaValue
from .may_v1 import _normalize_errors as _normalize_errors
from .model_field import ModelField as ModelField
from .shared import PYDANTIC_V2 as PYDANTIC_V2
from .shared import PYDANTIC_VERSION_MINOR_TUPLE as PYDANTIC_VERSION_MINOR_TUPLE
from .shared import annotation_is_pydantic_v1 as annotation_is_pydantic_v1
from .shared import field_annotation_is_scalar as field_annotation_is_scalar
from .shared import is_pydantic_v1_model_class as is_pydantic_v1_model_class
from .shared import is_pydantic_v1_model_instance as is_pydantic_v1_model_instance
from .shared import (
is_uploadfile_or_nonable_uploadfile_annotation as is_uploadfile_or_nonable_uploadfile_annotation,
)
@@ -47,3 +42,5 @@ from .shared import (
from .shared import lenient_issubclass as lenient_issubclass
from .shared import sequence_types as sequence_types
from .shared import value_is_sequence as value_is_sequence
from .v2 import ModelField as ModelField
from .v2 import get_definitions as get_definitions

View File

@@ -1,20 +1,18 @@
import sys
from collections.abc import Sequence
from functools import lru_cache
from typing import (
Any,
)
from fastapi._compat import may_v1
from fastapi._compat.shared import lenient_issubclass
from fastapi.types import ModelNameMap
from pydantic import BaseModel
from typing_extensions import Literal
from . import v2
from .model_field import ModelField
from .v2 import BaseConfig as BaseConfig
from .v2 import FieldInfo as FieldInfo
from .v2 import ModelField
from .v2 import PydanticSchemaGenerationError as PydanticSchemaGenerationError
from .v2 import RequiredParam as RequiredParam
from .v2 import Undefined as Undefined
@@ -23,6 +21,7 @@ from .v2 import Url as Url
from .v2 import Validator as Validator
from .v2 import evaluate_forwardref as evaluate_forwardref
from .v2 import get_missing_field_error as get_missing_field_error
from .v2 import get_model_fields as get_model_fields
from .v2 import (
with_info_plain_validator_function as with_info_plain_validator_function,
)
@@ -30,109 +29,50 @@ from .v2 import (
@lru_cache
def get_cached_model_fields(model: type[BaseModel]) -> list[ModelField]:
if lenient_issubclass(model, may_v1.BaseModel):
from fastapi._compat import v1
return v1.get_model_fields(model) # type: ignore[arg-type,return-value]
else:
from . import v2
return v2.get_model_fields(model) # type: ignore[return-value]
return get_model_fields(model) # type: ignore[return-value]
def _is_undefined(value: object) -> bool:
if isinstance(value, may_v1.UndefinedType):
return True
return isinstance(value, v2.UndefinedType)
def _get_model_config(model: BaseModel) -> Any:
if isinstance(model, may_v1.BaseModel):
from fastapi._compat import v1
return v1._get_model_config(model)
return v2._get_model_config(model)
def _model_dump(
model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
) -> Any:
if isinstance(model, may_v1.BaseModel):
from fastapi._compat import v1
return v1._model_dump(model, mode=mode, **kwargs)
return v2._model_dump(model, mode=mode, **kwargs)
def _is_error_wrapper(exc: Exception) -> bool:
if isinstance(exc, may_v1.ErrorWrapper):
return True
return isinstance(exc, v2.ErrorWrapper)
def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
if isinstance(field_info, may_v1.FieldInfo):
from fastapi._compat import v1
return v1.copy_field_info(field_info=field_info, annotation=annotation)
return v2.copy_field_info(field_info=field_info, annotation=annotation)
def create_body_model(
*, fields: Sequence[ModelField], model_name: str
) -> type[BaseModel]:
if fields and isinstance(fields[0], may_v1.ModelField):
from fastapi._compat import v1
return v1.create_body_model(fields=fields, model_name=model_name)
return v2.create_body_model(fields=fields, model_name=model_name) # type: ignore[arg-type]
def get_annotation_from_field_info(
annotation: Any, field_info: FieldInfo, field_name: str
) -> Any:
if isinstance(field_info, may_v1.FieldInfo):
from fastapi._compat import v1
return v1.get_annotation_from_field_info(
annotation=annotation, field_info=field_info, field_name=field_name
)
return v2.get_annotation_from_field_info(
annotation=annotation, field_info=field_info, field_name=field_name
)
def is_bytes_field(field: ModelField) -> bool:
if isinstance(field, may_v1.ModelField):
from fastapi._compat import v1
return v1.is_bytes_field(field)
return v2.is_bytes_field(field) # type: ignore[arg-type]
def is_bytes_sequence_field(field: ModelField) -> bool:
if isinstance(field, may_v1.ModelField):
from fastapi._compat import v1
return v1.is_bytes_sequence_field(field)
return v2.is_bytes_sequence_field(field) # type: ignore[arg-type]
def is_scalar_field(field: ModelField) -> bool:
if isinstance(field, may_v1.ModelField):
from fastapi._compat import v1
return v1.is_scalar_field(field)
return v2.is_scalar_field(field) # type: ignore[arg-type]
@@ -141,37 +81,15 @@ def is_scalar_sequence_field(field: ModelField) -> bool:
def is_sequence_field(field: ModelField) -> bool:
if isinstance(field, may_v1.ModelField):
from fastapi._compat import v1
return v1.is_sequence_field(field)
return v2.is_sequence_field(field) # type: ignore[arg-type]
def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
if isinstance(field, may_v1.ModelField):
from fastapi._compat import v1
return v1.serialize_sequence_value(field=field, value=value)
return v2.serialize_sequence_value(field=field, value=value) # type: ignore[arg-type]
def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap:
v1_model_fields = [
field for field in fields if isinstance(field, may_v1.ModelField)
]
if v1_model_fields:
from fastapi._compat import v1
v1_flat_models = v1.get_flat_models_from_fields(
v1_model_fields, # type: ignore[arg-type]
known_models=set(),
)
all_flat_models = v1_flat_models
else:
all_flat_models = set()
all_flat_models = set()
v2_model_fields = [field for field in fields if isinstance(field, v2.ModelField)]
v2_flat_models = v2.get_flat_models_from_fields(v2_model_fields, known_models=set())
@@ -181,67 +99,16 @@ def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap:
return model_name_map
def get_definitions(
*,
fields: list[ModelField],
model_name_map: ModelNameMap,
separate_input_output_schemas: bool = True,
) -> tuple[
dict[
tuple[ModelField, Literal["validation", "serialization"]],
may_v1.JsonSchemaValue,
],
dict[str, dict[str, Any]],
]:
if sys.version_info < (3, 14):
v1_fields = [field for field in fields if isinstance(field, may_v1.ModelField)]
v1_field_maps, v1_definitions = may_v1.get_definitions(
fields=v1_fields, # type: ignore[arg-type]
model_name_map=model_name_map,
separate_input_output_schemas=separate_input_output_schemas,
)
v2_fields = [field for field in fields if isinstance(field, v2.ModelField)]
v2_field_maps, v2_definitions = v2.get_definitions(
fields=v2_fields,
model_name_map=model_name_map,
separate_input_output_schemas=separate_input_output_schemas,
)
all_definitions = {**v1_definitions, **v2_definitions}
all_field_maps = {**v1_field_maps, **v2_field_maps} # type: ignore[misc]
return all_field_maps, all_definitions
# Pydantic v1 is not supported since Python 3.14
else:
v2_fields = [field for field in fields if isinstance(field, v2.ModelField)]
v2_field_maps, v2_definitions = v2.get_definitions(
fields=v2_fields,
model_name_map=model_name_map,
separate_input_output_schemas=separate_input_output_schemas,
)
return v2_field_maps, v2_definitions
def get_schema_from_model_field(
*,
field: ModelField,
model_name_map: ModelNameMap,
field_mapping: dict[
tuple[ModelField, Literal["validation", "serialization"]],
may_v1.JsonSchemaValue,
dict[str, Any],
],
separate_input_output_schemas: bool = True,
) -> dict[str, Any]:
if isinstance(field, may_v1.ModelField):
from fastapi._compat import v1
return v1.get_schema_from_model_field(
field=field,
model_name_map=model_name_map,
field_mapping=field_mapping,
separate_input_output_schemas=separate_input_output_schemas,
)
return v2.get_schema_from_model_field(
field=field, # type: ignore[arg-type]
model_name_map=model_name_map,
@@ -251,14 +118,8 @@ def get_schema_from_model_field(
def _is_model_field(value: Any) -> bool:
if isinstance(value, may_v1.ModelField):
return True
return isinstance(value, v2.ModelField)
def _is_model_class(value: Any) -> bool:
if lenient_issubclass(value, may_v1.BaseModel):
return True
return lenient_issubclass(value, v2.BaseModel) # type: ignore[attr-defined]

View File

@@ -1,124 +1,12 @@
import sys
from collections.abc import Sequence
from typing import Any, Literal, Union
from fastapi.types import ModelNameMap
if sys.version_info >= (3, 14):
class AnyUrl:
pass
class BaseConfig:
pass
class BaseModel:
pass
class Color:
pass
class CoreSchema:
pass
class ErrorWrapper:
pass
class FieldInfo:
pass
class GetJsonSchemaHandler:
pass
class JsonSchemaValue:
pass
class ModelField:
pass
class NameEmail:
pass
class RequiredParam:
pass
class SecretBytes:
pass
class SecretStr:
pass
class Undefined:
pass
class UndefinedType:
pass
class Url:
pass
from .v2 import ValidationError, create_model
def get_definitions(
*,
fields: list[ModelField],
model_name_map: ModelNameMap,
separate_input_output_schemas: bool = True,
) -> tuple[
dict[
tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
],
dict[str, dict[str, Any]],
]:
return {}, {} # pragma: no cover
else:
from .v1 import AnyUrl as AnyUrl
from .v1 import BaseConfig as BaseConfig
from .v1 import BaseModel as BaseModel
from .v1 import Color as Color
from .v1 import CoreSchema as CoreSchema
from .v1 import ErrorWrapper as ErrorWrapper
from .v1 import FieldInfo as FieldInfo
from .v1 import GetJsonSchemaHandler as GetJsonSchemaHandler
from .v1 import JsonSchemaValue as JsonSchemaValue
from .v1 import ModelField as ModelField
from .v1 import NameEmail as NameEmail
from .v1 import RequiredParam as RequiredParam
from .v1 import SecretBytes as SecretBytes
from .v1 import SecretStr as SecretStr
from .v1 import Undefined as Undefined
from .v1 import UndefinedType as UndefinedType
from .v1 import Url as Url
from .v1 import ValidationError, create_model
from .v1 import get_definitions as get_definitions
RequestErrorModel: type[BaseModel] = create_model("Request")
def _normalize_errors(errors: Sequence[Any]) -> list[dict[str, Any]]:
use_errors: list[Any] = []
for error in errors:
if isinstance(error, ErrorWrapper):
new_errors = ValidationError(
errors=[error], model=RequestErrorModel
).errors()
use_errors.extend(new_errors)
elif isinstance(error, list):
use_errors.extend(_normalize_errors(error))
else:
use_errors.append(error)
return use_errors
from typing import Any, Union
def _regenerate_error_with_loc(
*, errors: Sequence[Any], loc_prefix: tuple[Union[str, int], ...]
) -> list[dict[str, Any]]:
updated_loc_errors: list[Any] = [
{**err, "loc": loc_prefix + err.get("loc", ())}
for err in _normalize_errors(errors)
{**err, "loc": loc_prefix + err.get("loc", ())} for err in errors
]
return updated_loc_errors

View File

@@ -1,50 +0,0 @@
from typing import (
Any,
Union,
)
from fastapi.types import IncEx
from pydantic.fields import FieldInfo
from typing_extensions import Literal, Protocol
class ModelField(Protocol):
field_info: "FieldInfo"
name: str
mode: Literal["validation", "serialization"] = "validation"
_version: Literal["v1", "v2"] = "v1"
@property
def alias(self) -> str: ...
@property
def required(self) -> bool: ...
@property
def default(self) -> Any: ...
@property
def type_(self) -> Any: ...
def get_default(self) -> Any: ...
def validate(
self,
value: Any,
values: dict[str, Any] = {}, # noqa: B006
*,
loc: tuple[Union[int, str], ...] = (),
) -> tuple[Any, Union[list[dict[str, Any]], None]]: ...
def serialize(
self,
value: Any,
*,
mode: Literal["json", "python"] = "json",
include: Union[IncEx, None] = None,
exclude: Union[IncEx, None] = None,
by_alias: bool = True,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
) -> Any: ...

View File

@@ -1,6 +1,7 @@
import sys
import types
import typing
import warnings
from collections import deque
from collections.abc import Mapping, Sequence
from dataclasses import is_dataclass
@@ -10,7 +11,6 @@ from typing import (
Union,
)
from fastapi._compat import may_v1
from fastapi.types import UnionType
from pydantic import BaseModel
from pydantic.version import VERSION as PYDANTIC_VERSION
@@ -81,9 +81,7 @@ def value_is_sequence(value: Any) -> bool:
def _annotation_is_complex(annotation: Union[type[Any], None]) -> bool:
return (
lenient_issubclass(
annotation, (BaseModel, may_v1.BaseModel, Mapping, UploadFile)
)
lenient_issubclass(annotation, (BaseModel, Mapping, UploadFile))
or _annotation_is_sequence(annotation)
or is_dataclass(annotation)
)
@@ -179,13 +177,27 @@ def is_uploadfile_sequence_annotation(annotation: Any) -> bool:
)
def is_pydantic_v1_model_instance(obj: Any) -> bool:
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
from pydantic import v1
return isinstance(obj, v1.BaseModel)
def is_pydantic_v1_model_class(cls: Any) -> bool:
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
from pydantic import v1
return lenient_issubclass(cls, v1.BaseModel)
def annotation_is_pydantic_v1(annotation: Any) -> bool:
if lenient_issubclass(annotation, may_v1.BaseModel):
if is_pydantic_v1_model_class(annotation):
return True
origin = get_origin(annotation)
if origin is Union or origin is UnionType:
for arg in get_args(annotation):
if lenient_issubclass(arg, may_v1.BaseModel):
if is_pydantic_v1_model_class(arg):
return True
if field_annotation_is_sequence(annotation):
for sub_annotation in get_args(annotation):

View File

@@ -1,222 +0,0 @@
from collections.abc import Sequence
from copy import copy
from dataclasses import dataclass, is_dataclass
from enum import Enum
from typing import (
Any,
Callable,
Union,
)
from fastapi._compat import shared
from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX
from fastapi.types import ModelNameMap
from pydantic.v1 import BaseConfig as BaseConfig
from pydantic.v1 import BaseModel as BaseModel
from pydantic.v1 import ValidationError as ValidationError
from pydantic.v1 import create_model as create_model
from pydantic.v1.class_validators import Validator as Validator
from pydantic.v1.color import Color as Color
from pydantic.v1.error_wrappers import ErrorWrapper as ErrorWrapper
from pydantic.v1.fields import (
SHAPE_FROZENSET,
SHAPE_LIST,
SHAPE_SEQUENCE,
SHAPE_SET,
SHAPE_SINGLETON,
SHAPE_TUPLE,
SHAPE_TUPLE_ELLIPSIS,
)
from pydantic.v1.fields import FieldInfo as FieldInfo
from pydantic.v1.fields import ModelField as ModelField
from pydantic.v1.fields import Undefined as Undefined
from pydantic.v1.fields import UndefinedType as UndefinedType
from pydantic.v1.networks import AnyUrl as AnyUrl
from pydantic.v1.networks import NameEmail as NameEmail
from pydantic.v1.schema import TypeModelSet as TypeModelSet
from pydantic.v1.schema import field_schema, model_process_schema
from pydantic.v1.schema import (
get_annotation_from_field_info as get_annotation_from_field_info,
)
from pydantic.v1.schema import (
get_flat_models_from_field as get_flat_models_from_field,
)
from pydantic.v1.schema import (
get_flat_models_from_fields as get_flat_models_from_fields,
)
from pydantic.v1.schema import get_model_name_map as get_model_name_map
from pydantic.v1.types import SecretBytes as SecretBytes
from pydantic.v1.types import SecretStr as SecretStr
from pydantic.v1.typing import evaluate_forwardref as evaluate_forwardref
from pydantic.v1.utils import lenient_issubclass as lenient_issubclass
from pydantic.version import VERSION as PYDANTIC_VERSION
from typing_extensions import Literal
PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2])
PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2
# Keeping old "Required" functionality from Pydantic V1, without
# shadowing typing.Required.
RequiredParam: Any = Ellipsis
GetJsonSchemaHandler = Any
JsonSchemaValue = dict[str, Any]
CoreSchema = Any
Url = AnyUrl
sequence_shapes = {
SHAPE_LIST,
SHAPE_SET,
SHAPE_FROZENSET,
SHAPE_TUPLE,
SHAPE_SEQUENCE,
SHAPE_TUPLE_ELLIPSIS,
}
sequence_shape_to_type = {
SHAPE_LIST: list,
SHAPE_SET: set,
SHAPE_TUPLE: tuple,
SHAPE_SEQUENCE: list,
SHAPE_TUPLE_ELLIPSIS: list,
}
@dataclass
class GenerateJsonSchema:
ref_template: str
class PydanticSchemaGenerationError(Exception):
pass
RequestErrorModel: type[BaseModel] = create_model("Request")
def with_info_plain_validator_function(
function: Callable[..., Any],
*,
ref: Union[str, None] = None,
metadata: Any = None,
serialization: Any = None,
) -> Any:
return {}
def get_model_definitions(
*,
flat_models: set[Union[type[BaseModel], type[Enum]]],
model_name_map: dict[Union[type[BaseModel], type[Enum]], str],
) -> dict[str, Any]:
definitions: dict[str, dict[str, Any]] = {}
for model in flat_models:
m_schema, m_definitions, m_nested_models = model_process_schema(
model, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)
definitions.update(m_definitions)
model_name = model_name_map[model]
definitions[model_name] = m_schema
for m_schema in definitions.values():
if "description" in m_schema:
m_schema["description"] = m_schema["description"].split("\f")[0]
return definitions
def is_pv1_scalar_field(field: ModelField) -> bool:
from fastapi import params
field_info = field.field_info
if not (
field.shape == SHAPE_SINGLETON
and not lenient_issubclass(field.type_, BaseModel)
and not lenient_issubclass(field.type_, dict)
and not shared.field_annotation_is_sequence(field.type_)
and not is_dataclass(field.type_)
and not isinstance(field_info, params.Body)
):
return False
if field.sub_fields:
if not all(is_pv1_scalar_field(f) for f in field.sub_fields):
return False
return True
def _model_dump(
model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
) -> Any:
return model.dict(**kwargs)
def _get_model_config(model: BaseModel) -> Any:
return model.__config__
def get_schema_from_model_field(
*,
field: ModelField,
model_name_map: ModelNameMap,
field_mapping: dict[
tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
],
separate_input_output_schemas: bool = True,
) -> dict[str, Any]:
return field_schema(
field,
model_name_map=model_name_map, # type: ignore[arg-type]
ref_prefix=REF_PREFIX,
)[0]
# def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
# models = get_flat_models_from_fields(fields, known_models=set())
# return get_model_name_map(models) # type: ignore[no-any-return]
def get_definitions(
*,
fields: list[ModelField],
model_name_map: ModelNameMap,
separate_input_output_schemas: bool = True,
) -> tuple[
dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue],
dict[str, dict[str, Any]],
]:
models = get_flat_models_from_fields(fields, known_models=set())
return {}, get_model_definitions(flat_models=models, model_name_map=model_name_map) # type: ignore[arg-type]
def is_scalar_field(field: ModelField) -> bool:
return is_pv1_scalar_field(field)
def is_sequence_field(field: ModelField) -> bool:
return field.shape in sequence_shapes or shared._annotation_is_sequence(field.type_)
def is_bytes_field(field: ModelField) -> bool:
return lenient_issubclass(field.type_, bytes)
def is_bytes_sequence_field(field: ModelField) -> bool:
return field.shape in sequence_shapes and lenient_issubclass(field.type_, bytes)
def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
return copy(field_info)
def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
return sequence_shape_to_type[field.shape](value) # type: ignore[no-any-return]
def create_body_model(
*, fields: Sequence[ModelField], model_name: str
) -> type[BaseModel]:
BodyModel = create_model(model_name)
for f in fields:
BodyModel.__fields__[f.name] = f
return BodyModel
def get_model_fields(model: type[BaseModel]) -> list[ModelField]:
return list(model.__fields__.values())

View File

@@ -503,19 +503,9 @@ def normalize_name(name: str) -> str:
def get_model_name_map(unique_models: TypeModelSet) -> dict[TypeModelOrEnum, str]:
name_model_map = {}
conflicting_names: set[str] = set()
for model in unique_models:
model_name = normalize_name(model.__name__)
if model_name in conflicting_names:
model_name = get_long_model_name(model)
name_model_map[model_name] = model
elif model_name in name_model_map:
conflicting_names.add(model_name)
conflicting_model = name_model_map.pop(model_name)
name_model_map[get_long_model_name(conflicting_model)] = conflicting_model
name_model_map[get_long_model_name(model)] = model
else:
name_model_map[model_name] = model
name_model_map[model_name] = model
return {v: k for k, v in name_model_map.items()}
@@ -565,7 +555,3 @@ def get_flat_models_from_fields(
for field in fields:
get_flat_models_from_field(field, known_models=known_models)
return known_models
def get_long_model_name(model: TypeModelOrEnum) -> str:
return f"{model.__module__}__{model.__qualname__}".replace(".", "__")

View File

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

View File

@@ -1,7 +1,6 @@
import dataclasses
import inspect
import sys
import warnings
from collections.abc import Coroutine, Mapping, Sequence
from contextlib import AsyncExitStack, contextmanager
from copy import copy, deepcopy
@@ -22,7 +21,6 @@ from fastapi._compat import (
ModelField,
RequiredParam,
Undefined,
_is_error_wrapper,
_is_model_class,
copy_field_info,
create_body_model,
@@ -44,14 +42,13 @@ from fastapi._compat import (
serialize_sequence_value,
value_is_sequence,
)
from fastapi._compat.shared import annotation_is_pydantic_v1
from fastapi.background import BackgroundTasks
from fastapi.concurrency import (
asynccontextmanager,
contextmanager_in_threadpool,
)
from fastapi.dependencies.models import Dependant
from fastapi.exceptions import DependencyScopeError, FastAPIDeprecationWarning
from fastapi.exceptions import DependencyScopeError
from fastapi.logger import logger
from fastapi.security.oauth2 import SecurityScopes
from fastapi.types import DependencyCacheKey
@@ -72,8 +69,6 @@ from starlette.responses import Response
from starlette.websockets import WebSocket
from typing_extensions import Literal, get_args, get_origin
from .. import temp_pydantic_v1_params
multipart_not_installed_error = (
'Form data requires "python-multipart" to be installed. \n'
'You can install "python-multipart" with: \n\n'
@@ -323,16 +318,7 @@ def get_dependant(
)
continue
assert param_details.field is not None
if isinstance(param_details.field, may_v1.ModelField):
warnings.warn(
"pydantic.v1 is deprecated and will soon stop being supported by FastAPI."
f" Please update the param {param_name}: {param_details.type_annotation!r}.",
category=FastAPIDeprecationWarning,
stacklevel=5,
)
if isinstance(
param_details.field.field_info, (params.Body, temp_pydantic_v1_params.Body)
):
if isinstance(param_details.field.field_info, params.Body):
dependant.body_params.append(param_details.field)
else:
add_param_to_fields(field=param_details.field, dependant=dependant)
@@ -391,7 +377,7 @@ def analyze_param(
fastapi_annotations = [
arg
for arg in annotated_args[1:]
if isinstance(arg, (FieldInfo, may_v1.FieldInfo, params.Depends))
if isinstance(arg, (FieldInfo, params.Depends))
]
fastapi_specific_annotations = [
arg
@@ -400,30 +386,27 @@ def analyze_param(
arg,
(
params.Param,
temp_pydantic_v1_params.Param,
params.Body,
temp_pydantic_v1_params.Body,
params.Depends,
),
)
]
if fastapi_specific_annotations:
fastapi_annotation: Union[
FieldInfo, may_v1.FieldInfo, params.Depends, None
] = fastapi_specific_annotations[-1]
fastapi_annotation: Union[FieldInfo, params.Depends, None] = (
fastapi_specific_annotations[-1]
)
else:
fastapi_annotation = None
# 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.
field_info = copy_field_info(
field_info=fastapi_annotation, # type: ignore[arg-type]
annotation=use_annotation,
)
assert field_info.default in {
Undefined,
may_v1.Undefined,
} or field_info.default in {RequiredParam, may_v1.RequiredParam}, (
assert (
field_info.default == Undefined or field_info.default == RequiredParam
), (
f"`{field_info.__class__.__name__}` default value cannot be set in"
f" `Annotated` for {param_name!r}. Set the default value with `=` instead."
)
@@ -447,7 +430,7 @@ def analyze_param(
)
depends = value
# Get FieldInfo from default value
elif isinstance(value, (FieldInfo, may_v1.FieldInfo)):
elif isinstance(value, FieldInfo):
assert field_info is None, (
"Cannot specify FastAPI annotations in `Annotated` and default value"
f" together for {param_name!r}"
@@ -491,14 +474,7 @@ def analyze_param(
) or is_uploadfile_sequence_annotation(type_annotation):
field_info = params.File(annotation=use_annotation, default=default_value)
elif not field_annotation_is_scalar(annotation=type_annotation):
if annotation_is_pydantic_v1(use_annotation):
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
)
field_info = params.Body(annotation=use_annotation, default=default_value)
else:
field_info = params.Query(annotation=use_annotation, default=default_value)
@@ -507,14 +483,12 @@ def analyze_param(
if field_info is not None:
# Handle field_info.in_
if is_path_param:
assert isinstance(
field_info, (params.Path, temp_pydantic_v1_params.Path)
), (
assert isinstance(field_info, params.Path), (
f"Cannot use `{field_info.__class__.__name__}` for path param"
f" {param_name!r}"
)
elif (
isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param))
isinstance(field_info, params.Param)
and getattr(field_info, "in_", None) is None
):
field_info.in_ = params.ParamTypes.query
@@ -523,7 +497,7 @@ def analyze_param(
field_info,
param_name,
)
if isinstance(field_info, (params.Form, temp_pydantic_v1_params.Form)):
if isinstance(field_info, params.Form):
ensure_multipart_is_installed()
if not field_info.alias and getattr(field_info, "convert_underscores", None):
alias = param_name.replace("_", "-")
@@ -535,15 +509,14 @@ def analyze_param(
type_=use_annotation_from_field_info,
default=field_info.default,
alias=alias,
required=field_info.default
in (RequiredParam, may_v1.RequiredParam, Undefined),
required=field_info.default in (RequiredParam, Undefined),
field_info=field_info,
)
if is_path_param:
assert is_scalar_field(field=field), (
"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 (
is_scalar_field(field)
or is_scalar_sequence_field(field)
@@ -742,9 +715,7 @@ def _validate_value_with_model_field(
else:
return deepcopy(field.default), []
v_, errors_ = field.validate(value, values, loc=loc)
if _is_error_wrapper(errors_): # type: ignore[arg-type]
return None, [errors_]
elif isinstance(errors_, list):
if isinstance(errors_, list):
new_errors = may_v1._regenerate_error_with_loc(errors=errors_, loc_prefix=())
return None, new_errors
else:
@@ -762,7 +733,7 @@ def _get_multidict_value(
if (
value is None
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 value == ""
)
@@ -832,7 +803,7 @@ def request_params_to_args(
if single_not_embedded_field:
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"
)
loc: tuple[str, ...] = (field_info.in_.value,)
@@ -844,7 +815,7 @@ def request_params_to_args(
for field in fields:
value = _get_multidict_value(field, received_params)
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"
)
loc = (field_info.in_.value, get_validation_alias(field))
@@ -893,7 +864,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
# otherwise it has to be embedded, so that the key value pair can be extracted
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_union_of_base_models(first_field.type_)
):
@@ -911,14 +882,14 @@ async def _extract_form_body(
value = _get_multidict_value(field, received_body)
field_info = field.field_info
if (
isinstance(field_info, (params.File, temp_pydantic_v1_params.File))
isinstance(field_info, params.File)
and is_bytes_field(field)
and isinstance(value, UploadFile)
):
value = await value.read()
elif (
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)
):
# For types
@@ -1029,28 +1000,15 @@ def get_body_field(
BodyFieldInfo_kwargs["default"] = None
if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params):
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):
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:
if annotation_is_pydantic_v1(BodyModel):
BodyFieldInfo = temp_pydantic_v1_params.Body # type: ignore[assignment]
else:
BodyFieldInfo = params.Body
BodyFieldInfo = params.Body
body_param_media_types = [
f.field_info.media_type
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:
BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0]

View File

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

View File

@@ -233,6 +233,12 @@ class ResponseValidationError(ValidationException):
self.body = body
class PydanticV1NotSupportedError(FastAPIError):
"""
A pydantic.v1 model is used, which is no longer supported.
"""
class FastAPIDeprecationWarning(UserWarning):
"""
A custom deprecation warning as DeprecationWarning is ignored

View File

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

View File

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

View File

@@ -2,7 +2,6 @@ import email.message
import functools
import inspect
import json
import warnings
from collections.abc import (
AsyncIterator,
Awaitable,
@@ -22,16 +21,12 @@ from typing import (
)
from annotated_doc import Doc
from fastapi import params, temp_pydantic_v1_params
from fastapi import params
from fastapi._compat import (
ModelField,
Undefined,
_get_model_config,
_model_dump,
_normalize_errors,
annotation_is_pydantic_v1,
lenient_issubclass,
may_v1,
)
from fastapi.datastructures import Default, DefaultPlaceholder
from fastapi.dependencies.models import Dependant
@@ -47,8 +42,8 @@ from fastapi.dependencies.utils import (
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import (
EndpointContext,
FastAPIDeprecationWarning,
FastAPIError,
PydanticV1NotSupportedError,
RequestValidationError,
ResponseValidationError,
WebSocketRequestValidationError,
@@ -148,51 +143,6 @@ def websocket_session(
return app
def _prepare_response_content(
res: Any,
*,
exclude_unset: bool,
exclude_defaults: bool = False,
exclude_none: bool = False,
) -> Any:
if isinstance(res, may_v1.BaseModel):
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 [
_prepare_response_content(
item,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
for item in res
]
elif isinstance(res, dict):
return {
k: _prepare_response_content(
v,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
for k, v in res.items()
}
return res
def _merge_lifespan_context(
original_context: Lifespan[Any], nested_context: Lifespan[Any]
) -> Lifespan[Any]:
@@ -252,14 +202,6 @@ async def serialize_response(
) -> Any:
if field:
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:
value, errors_ = field.validate(response_content, {}, loc=("response",))
else:
@@ -268,28 +210,15 @@ async def serialize_response(
)
if isinstance(errors_, list):
errors.extend(errors_)
elif errors_:
errors.append(errors_)
if errors:
ctx = endpoint_ctx or EndpointContext()
raise ResponseValidationError(
errors=_normalize_errors(errors),
errors=errors,
body=response_content,
endpoint_ctx=ctx,
)
if hasattr(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(
return field.serialize(
value,
include=include,
exclude=exclude,
@@ -298,6 +227,7 @@ async def serialize_response(
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
else:
return jsonable_encoder(response_content)
@@ -332,9 +262,7 @@ def get_request_handler(
) -> Callable[[Request], Coroutine[Any, Any, Response]]:
assert dependant.call is not None, "dependant.call must be a function"
is_coroutine = dependant.is_coroutine_callable
is_body_form = body_field and isinstance(
body_field.field_info, (params.Form, temp_pydantic_v1_params.Form)
)
is_body_form = body_field and isinstance(body_field.field_info, params.Form)
if isinstance(response_class, DefaultPlaceholder):
actual_response_class: type[Response] = response_class.value
else:
@@ -464,7 +392,7 @@ def get_request_handler(
response.headers.raw.extend(solved_result.response.headers.raw)
if errors:
validation_error = RequestValidationError(
_normalize_errors(errors), body=body, endpoint_ctx=endpoint_ctx
errors, body=body, endpoint_ctx=endpoint_ctx
)
raise validation_error
@@ -503,7 +431,7 @@ def get_websocket_app(
)
if solved_result.errors:
raise WebSocketRequestValidationError(
_normalize_errors(solved_result.errors),
solved_result.errors,
endpoint_ctx=endpoint_ctx,
)
assert dependant.call is not None, "dependant.call must be a function"
@@ -638,11 +566,9 @@ class APIRoute(routing.Route):
)
response_name = "Response_" + self.unique_id
if annotation_is_pydantic_v1(self.response_model):
warnings.warn(
"pydantic.v1 is deprecated and will soon stop being supported by FastAPI."
f" Please update the response model {self.response_model!r}.",
category=FastAPIDeprecationWarning,
stacklevel=4,
raise PydanticV1NotSupportedError(
"pydantic.v1 models are no longer supported by FastAPI."
f" Please update the response model {self.response_model!r}."
)
self.response_field = create_model_field(
name=response_name,
@@ -678,11 +604,9 @@ class APIRoute(routing.Route):
)
response_name = f"Response_{additional_status_code}_{self.unique_id}"
if annotation_is_pydantic_v1(model):
warnings.warn(
"pydantic.v1 is deprecated and will soon stop being supported by FastAPI."
f" In responses={{}}, please update {model}.",
category=FastAPIDeprecationWarning,
stacklevel=4,
raise PydanticV1NotSupportedError(
"pydantic.v1 models are no longer supported by FastAPI."
f" In responses={{}}, please update {model}."
)
response_field = create_model_field(
name=response_name, type_=model, mode="serialization"

View File

@@ -1,718 +0,0 @@
import warnings
from typing import Annotated, Any, Callable, Optional, Union
from fastapi.exceptions import FastAPIDeprecationWarning
from fastapi.openapi.models import Example
from fastapi.params import ParamTypes
from typing_extensions import deprecated
from ._compat.may_v1 import FieldInfo, Undefined
_Unset: Any = Undefined
class Param(FieldInfo):
in_: ParamTypes
def __init__(
self,
default: Any = Undefined,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
annotation: Optional[Any] = None,
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
ge: Optional[float] = None,
lt: Optional[float] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
examples: Optional[list[Any]] = None,
example: Annotated[
Optional[Any],
deprecated(
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
openapi_examples: Optional[dict[str, Example]] = None,
deprecated: Union[deprecated, str, bool, None] = None,
include_in_schema: bool = True,
json_schema_extra: Union[dict[str, Any], None] = None,
**extra: Any,
):
if example is not _Unset:
warnings.warn(
"`example` has been deprecated, please use `examples` instead",
category=FastAPIDeprecationWarning,
stacklevel=4,
)
self.example = example
self.include_in_schema = include_in_schema
self.openapi_examples = openapi_examples
kwargs = dict(
default=default,
default_factory=default_factory,
alias=alias,
title=title,
description=description,
gt=gt,
ge=ge,
lt=lt,
le=le,
min_length=min_length,
max_length=max_length,
discriminator=discriminator,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
**extra,
)
if examples is not None:
kwargs["examples"] = examples
if regex is not None:
warnings.warn(
"`regex` has been deprecated, please use `pattern` instead",
category=FastAPIDeprecationWarning,
stacklevel=4,
)
current_json_schema_extra = json_schema_extra or extra
kwargs["deprecated"] = deprecated
kwargs["regex"] = pattern or regex
kwargs.update(**current_json_schema_extra)
use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset}
super().__init__(**use_kwargs)
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.default})"
class Path(Param):
in_ = ParamTypes.path
def __init__(
self,
default: Any = ...,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
annotation: Optional[Any] = None,
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
ge: Optional[float] = None,
lt: Optional[float] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
examples: Optional[list[Any]] = None,
example: Annotated[
Optional[Any],
deprecated(
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
openapi_examples: Optional[dict[str, Example]] = None,
deprecated: Union[deprecated, str, bool, None] = None,
include_in_schema: bool = True,
json_schema_extra: Union[dict[str, Any], None] = None,
**extra: Any,
):
assert default is ..., "Path parameters cannot have a default value"
self.in_ = self.in_
super().__init__(
default=default,
default_factory=default_factory,
annotation=annotation,
alias=alias,
alias_priority=alias_priority,
validation_alias=validation_alias,
serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
ge=ge,
lt=lt,
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
discriminator=discriminator,
strict=strict,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
deprecated=deprecated,
example=example,
examples=examples,
openapi_examples=openapi_examples,
include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra,
**extra,
)
class Query(Param):
in_ = ParamTypes.query
def __init__(
self,
default: Any = Undefined,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
annotation: Optional[Any] = None,
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
ge: Optional[float] = None,
lt: Optional[float] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
examples: Optional[list[Any]] = None,
example: Annotated[
Optional[Any],
deprecated(
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
openapi_examples: Optional[dict[str, Example]] = None,
deprecated: Union[deprecated, str, bool, None] = None,
include_in_schema: bool = True,
json_schema_extra: Union[dict[str, Any], None] = None,
**extra: Any,
):
super().__init__(
default=default,
default_factory=default_factory,
annotation=annotation,
alias=alias,
alias_priority=alias_priority,
validation_alias=validation_alias,
serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
ge=ge,
lt=lt,
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
discriminator=discriminator,
strict=strict,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
deprecated=deprecated,
example=example,
examples=examples,
openapi_examples=openapi_examples,
include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra,
**extra,
)
class Header(Param):
in_ = ParamTypes.header
def __init__(
self,
default: Any = Undefined,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
annotation: Optional[Any] = None,
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
convert_underscores: bool = True,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
ge: Optional[float] = None,
lt: Optional[float] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
examples: Optional[list[Any]] = None,
example: Annotated[
Optional[Any],
deprecated(
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
openapi_examples: Optional[dict[str, Example]] = None,
deprecated: Union[deprecated, str, bool, None] = None,
include_in_schema: bool = True,
json_schema_extra: Union[dict[str, Any], None] = None,
**extra: Any,
):
self.convert_underscores = convert_underscores
super().__init__(
default=default,
default_factory=default_factory,
annotation=annotation,
alias=alias,
alias_priority=alias_priority,
validation_alias=validation_alias,
serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
ge=ge,
lt=lt,
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
discriminator=discriminator,
strict=strict,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
deprecated=deprecated,
example=example,
examples=examples,
openapi_examples=openapi_examples,
include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra,
**extra,
)
class Cookie(Param):
in_ = ParamTypes.cookie
def __init__(
self,
default: Any = Undefined,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
annotation: Optional[Any] = None,
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
ge: Optional[float] = None,
lt: Optional[float] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
examples: Optional[list[Any]] = None,
example: Annotated[
Optional[Any],
deprecated(
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
openapi_examples: Optional[dict[str, Example]] = None,
deprecated: Union[deprecated, str, bool, None] = None,
include_in_schema: bool = True,
json_schema_extra: Union[dict[str, Any], None] = None,
**extra: Any,
):
super().__init__(
default=default,
default_factory=default_factory,
annotation=annotation,
alias=alias,
alias_priority=alias_priority,
validation_alias=validation_alias,
serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
ge=ge,
lt=lt,
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
discriminator=discriminator,
strict=strict,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
deprecated=deprecated,
example=example,
examples=examples,
openapi_examples=openapi_examples,
include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra,
**extra,
)
class Body(FieldInfo):
def __init__(
self,
default: Any = Undefined,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
annotation: Optional[Any] = None,
embed: Union[bool, None] = None,
media_type: str = "application/json",
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
ge: Optional[float] = None,
lt: Optional[float] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
examples: Optional[list[Any]] = None,
example: Annotated[
Optional[Any],
deprecated(
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
openapi_examples: Optional[dict[str, Example]] = None,
deprecated: Union[deprecated, str, bool, None] = None,
include_in_schema: bool = True,
json_schema_extra: Union[dict[str, Any], None] = None,
**extra: Any,
):
self.embed = embed
self.media_type = media_type
if example is not _Unset:
warnings.warn(
"`example` has been deprecated, please use `examples` instead",
category=FastAPIDeprecationWarning,
stacklevel=4,
)
self.example = example
self.include_in_schema = include_in_schema
self.openapi_examples = openapi_examples
kwargs = dict(
default=default,
default_factory=default_factory,
alias=alias,
title=title,
description=description,
gt=gt,
ge=ge,
lt=lt,
le=le,
min_length=min_length,
max_length=max_length,
discriminator=discriminator,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
**extra,
)
if examples is not None:
kwargs["examples"] = examples
if regex is not None:
warnings.warn(
"`regex` has been deprecated, please use `pattern` instead",
category=FastAPIDeprecationWarning,
stacklevel=4,
)
current_json_schema_extra = json_schema_extra or extra
kwargs["deprecated"] = deprecated
kwargs["regex"] = pattern or regex
kwargs.update(**current_json_schema_extra)
use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset}
super().__init__(**use_kwargs)
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.default})"
class Form(Body):
def __init__(
self,
default: Any = Undefined,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
annotation: Optional[Any] = None,
media_type: str = "application/x-www-form-urlencoded",
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
ge: Optional[float] = None,
lt: Optional[float] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
examples: Optional[list[Any]] = None,
example: Annotated[
Optional[Any],
deprecated(
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
openapi_examples: Optional[dict[str, Example]] = None,
deprecated: Union[deprecated, str, bool, None] = None,
include_in_schema: bool = True,
json_schema_extra: Union[dict[str, Any], None] = None,
**extra: Any,
):
super().__init__(
default=default,
default_factory=default_factory,
annotation=annotation,
media_type=media_type,
alias=alias,
alias_priority=alias_priority,
validation_alias=validation_alias,
serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
ge=ge,
lt=lt,
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
discriminator=discriminator,
strict=strict,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
deprecated=deprecated,
example=example,
examples=examples,
openapi_examples=openapi_examples,
include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra,
**extra,
)
class File(Form):
def __init__(
self,
default: Any = Undefined,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
annotation: Optional[Any] = None,
media_type: str = "multipart/form-data",
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
ge: Optional[float] = None,
lt: Optional[float] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
examples: Optional[list[Any]] = None,
example: Annotated[
Optional[Any],
deprecated(
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
openapi_examples: Optional[dict[str, Example]] = None,
deprecated: Union[deprecated, str, bool, None] = None,
include_in_schema: bool = True,
json_schema_extra: Union[dict[str, Any], None] = None,
**extra: Any,
):
super().__init__(
default=default,
default_factory=default_factory,
annotation=annotation,
media_type=media_type,
alias=alias,
alias_priority=alias_priority,
validation_alias=validation_alias,
serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
ge=ge,
lt=lt,
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
discriminator=discriminator,
strict=strict,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
deprecated=deprecated,
example=example,
examples=examples,
openapi_examples=openapi_examples,
include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra,
**extra,
)

View File

@@ -6,7 +6,6 @@ from typing import (
Any,
Optional,
Union,
cast,
)
from weakref import WeakKeyDictionary
@@ -19,11 +18,9 @@ from fastapi._compat import (
UndefinedType,
Validator,
annotation_is_pydantic_v1,
lenient_issubclass,
may_v1,
)
from fastapi.datastructures import DefaultPlaceholder, DefaultType
from fastapi.exceptions import FastAPIDeprecationWarning
from fastapi.exceptions import FastAPIDeprecationWarning, PydanticV1NotSupportedError
from pydantic import BaseModel
from pydantic.fields import FieldInfo
from typing_extensions import Literal
@@ -83,52 +80,18 @@ def create_model_field(
mode: Literal["validation", "serialization"] = "validation",
version: Literal["1", "auto"] = "auto",
) -> ModelField:
if annotation_is_pydantic_v1(type_):
raise PydanticV1NotSupportedError(
"pydantic.v1 models are no longer supported by FastAPI."
f" Please update the response model {type_!r}."
)
class_validators = class_validators or {}
v1_model_config = may_v1.BaseConfig
v1_field_info = field_info or may_v1.FieldInfo()
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}
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:
return v1.ModelField(**v1_kwargs)
except RuntimeError:
return v2.ModelField(**kwargs) # type: ignore[return-value,arg-type]
except PydanticSchemaGenerationError:
raise fastapi.exceptions.FastAPIError(
_invalid_args_message.format(type_=type_)
) from None
@@ -139,57 +102,7 @@ def create_cloned_field(
*,
cloned_types: Optional[MutableMapping[type[BaseModel], type[BaseModel]]] = None,
) -> ModelField:
if isinstance(field, v2.ModelField):
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
return field
def generate_operation_id_for_path(

View File

@@ -199,14 +199,19 @@ omit = [
"docs_src/dependencies/tutorial008_an_py39.py", # difficult to mock
"docs_src/dependencies/tutorial013_an_py310.py", # temporary code example?
"docs_src/dependencies/tutorial014_an_py310.py", # temporary code example?
# Pydantic V1
# Pydantic v1 migration, no longer tested
"docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py",
"docs_src/pydantic_v1_in_v2/tutorial001_an_py39.py",
"docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py",
"docs_src/pydantic_v1_in_v2/tutorial002_an_py39.py",
"docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py",
"docs_src/pydantic_v1_in_v2/tutorial003_an_py39.py",
"docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py",
"docs_src/pydantic_v1_in_v2/tutorial004_an_py39.py",
# TODO: remove when removing this file, after updating translations, Pydantic v1
"docs_src/schema_extra_example/tutorial001_pv1_py310.py",
"docs_src/query_param_models/tutorial002_pv1_py310.py",
"docs_src/query_param_models/tutorial002_pv1_an_py310.py",
"docs_src/header_param_models/tutorial002_pv1_py310.py",
"docs_src/header_param_models/tutorial002_pv1_an_py310.py",
"docs_src/cookie_param_models/tutorial002_pv1_py310.py",
"docs_src/cookie_param_models/tutorial002_pv1_an_py310.py",
"docs_src/schema_extra_example/tutorial001_pv1_py39.py",
"docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py",
]
[tool.coverage.report]

View File

@@ -1,20 +1,17 @@
from typing import Any, Union
from typing import Union
from fastapi import FastAPI, UploadFile
from fastapi._compat import (
Undefined,
_get_model_config,
get_cached_model_fields,
is_scalar_field,
is_uploadfile_sequence_annotation,
may_v1,
)
from fastapi._compat.shared import is_bytes_sequence_annotation
from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict
from pydantic.fields import FieldInfo
from .utils import needs_py310, needs_py_lt_314
from .utils import needs_py310
def test_model_field_default_required():
@@ -26,18 +23,6 @@ def test_model_field_default_required():
assert field.default is Undefined
@needs_py_lt_314
def test_v1_plain_validator_function():
from fastapi._compat import v1
# For coverage
def func(v): # pragma: no cover
return v
result = v1.with_info_plain_validator_function(func)
assert result == {}
def test_is_model_field():
# For coverage
from fastapi._compat import _is_model_field
@@ -165,33 +150,3 @@ def test_serialize_sequence_value_with_none_first_in_union():
result = v2.serialize_sequence_value(field=field, value=["x", "y"])
assert result == ["x", "y"]
assert isinstance(result, list)
@needs_py_lt_314
def test_is_pv1_scalar_field():
from fastapi._compat import v1
# For coverage
class Model(v1.BaseModel):
foo: Union[str, dict[str, Any]]
fields = v1.get_model_fields(Model)
assert not is_scalar_field(fields[0])
@needs_py_lt_314
def test_get_model_fields_cached():
from fastapi._compat import v1
class Model(may_v1.BaseModel):
foo: str
non_cached_fields = v1.get_model_fields(Model)
non_cached_fields2 = v1.get_model_fields(Model)
cached_fields = get_cached_model_fields(Model)
cached_fields2 = get_cached_model_fields(Model)
for f1, f2 in zip(cached_fields, cached_fields2):
assert f1 is f2
assert non_cached_fields is not non_cached_fields2
assert cached_fields is cached_fields2

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,9 @@
import warnings
from datetime import datetime, timezone
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
from .utils import needs_pydanticv1
def test_pydanticv2():
from pydantic import field_serializer
@@ -29,34 +26,3 @@ def test_pydanticv2():
with client:
response = client.get("/model")
assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"}
# TODO: remove when deprecating Pydantic v1
@needs_pydanticv1
def test_pydanticv1():
from pydantic import v1
class ModelWithDatetimeField(v1.BaseModel):
dt_field: datetime
class Config:
json_encoders = {
datetime: lambda dt: dt.replace(
microsecond=0, tzinfo=timezone.utc
).isoformat()
}
app = FastAPI()
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.get("/model", response_model=ModelWithDatetimeField)
def get_model():
return model
client = TestClient(app)
with client:
response = client.get("/model")
assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"}

View File

@@ -1,45 +0,0 @@
import warnings
from typing import Optional
from fastapi import Depends, FastAPI
from pydantic.v1 import BaseModel, validator
app = FastAPI()
class ModelB(BaseModel):
username: str
class ModelC(ModelB):
password: str
class ModelA(BaseModel):
name: str
description: Optional[str] = None
model_b: ModelB
tags: dict[str, str] = {}
@validator("name")
def lower_username(cls, name: str, values):
if not name.endswith("A"):
raise ValueError("name must end in A")
return name
async def get_model_c() -> ModelC:
return ModelC(username="test-user", password="test-password")
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.get("/model/{name}", response_model=ModelA)
async def get_model_a(name: str, model_c=Depends(get_model_c)):
return {
"name": name,
"description": "model-a-desc",
"model_b": model_c,
"tags": {"key1": "value1", "key2": "value2"},
}

View File

@@ -1,146 +0,0 @@
import pytest
from fastapi.exceptions import ResponseValidationError
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from ..utils import needs_pydanticv1
@pytest.fixture(name="client")
def get_client():
from .app_pv1 import app
client = TestClient(app)
return client
@needs_pydanticv1
def test_filter_sub_model(client: TestClient):
response = client.get("/model/modelA")
assert response.status_code == 200, response.text
assert response.json() == {
"name": "modelA",
"description": "model-a-desc",
"model_b": {"username": "test-user"},
"tags": {"key1": "value1", "key2": "value2"},
}
@needs_pydanticv1
def test_validator_is_cloned(client: TestClient):
with pytest.raises(ResponseValidationError) as err:
client.get("/model/modelX")
assert err.value.errors() == [
{
"loc": ("response", "name"),
"msg": "name must end in A",
"type": "value_error",
}
]
@needs_pydanticv1
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/model/{name}": {
"get": {
"summary": "Get Model A",
"operationId": "get_model_a_model__name__get",
"parameters": [
{
"required": True,
"schema": {"title": "Name", "type": "string"},
"name": "name",
"in": "path",
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ModelA"
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
}
},
"components": {
"schemas": {
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {
"$ref": "#/components/schemas/ValidationError"
},
}
},
},
"ModelA": {
"title": "ModelA",
"required": ["name", "model_b"],
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"description": {"title": "Description", "type": "string"},
"model_b": {"$ref": "#/components/schemas/ModelB"},
"tags": {
"additionalProperties": {"type": "string"},
"type": "object",
"title": "Tags",
"default": {},
},
},
},
"ModelB": {
"title": "ModelB",
"required": ["username"],
"type": "object",
"properties": {
"username": {"title": "Username", "type": "string"}
},
},
"ValidationError": {
"title": "ValidationError",
"required": ["loc", "msg", "type"],
"type": "object",
"properties": {
"loc": {
"title": "Location",
"type": "array",
"items": {
"anyOf": [{"type": "string"}, {"type": "integer"}]
},
},
"msg": {"title": "Message", "type": "string"},
"type": {"title": "Error Type", "type": "string"},
},
},
}
},
}
)

View File

@@ -1,25 +1,12 @@
import warnings
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from .utils import needs_pydanticv1
@pytest.fixture(
name="client",
params=[
pytest.param("pydantic-v1", marks=needs_pydanticv1),
"pydantic-v2",
],
)
def client_fixture(request: pytest.FixtureRequest) -> TestClient:
if request.param == "pydantic-v1":
from pydantic.v1 import BaseModel
else:
from pydantic import BaseModel
@pytest.fixture(name="client")
def client_fixture() -> TestClient:
from pydantic import BaseModel
class Address(BaseModel):
"""
@@ -38,28 +25,12 @@ def client_fixture(request: pytest.FixtureRequest) -> TestClient:
app = FastAPI()
if request.param == "pydantic-v1":
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.get("/facilities/{facility_id}")
def get_facility(facility_id: str) -> Facility:
return Facility(
id=facility_id,
address=Address(
line_1="123 Main St", city="Anytown", state_province="CA"
),
)
else:
@app.get("/facilities/{facility_id}")
def get_facility(facility_id: str) -> Facility:
return Facility(
id=facility_id,
address=Address(
line_1="123 Main St", city="Anytown", state_province="CA"
),
)
@app.get("/facilities/{facility_id}")
def get_facility(facility_id: str) -> Facility:
return Facility(
id=facility_id,
address=Address(line_1="123 Main St", city="Anytown", state_province="CA"),
)
client = TestClient(app)
return client

View File

@@ -5,8 +5,6 @@ from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
from .utils import needs_pydanticv1
class MyUuid:
def __init__(self, uuid_string: str):
@@ -67,46 +65,3 @@ def test_pydanticv2():
assert response_pydantic.json() == {
"a_uuid": "b8799909-f914-42de-91bc-95c819218d01"
}
# TODO: remove when deprecating Pydantic v1
@needs_pydanticv1
def test_pydanticv1():
from pydantic import v1
app = FastAPI()
@app.get("/fast_uuid")
def return_fast_uuid():
asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
assert isinstance(asyncpg_uuid, uuid.UUID)
assert type(asyncpg_uuid) is not uuid.UUID
with pytest.raises(TypeError):
vars(asyncpg_uuid)
return {"fast_uuid": asyncpg_uuid}
class SomeCustomClass(v1.BaseModel):
class Config:
arbitrary_types_allowed = True
json_encoders = {uuid.UUID: str}
a_uuid: MyUuid
@app.get("/get_custom_class")
def return_some_user():
# Test that the fix also works for custom pydantic classes
return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
client = TestClient(app)
with client:
response_simple = client.get("/fast_uuid")
response_pydantic = client.get("/get_custom_class")
assert response_simple.json() == {
"fast_uuid": "a10ff360-3b1e-4984-a26f-d3ab460bdb51"
}
assert response_pydantic.json() == {
"a_uuid": "b8799909-f914-42de-91bc-95c819218d01"
}

View File

@@ -1,3 +1,4 @@
import warnings
from collections import deque
from dataclasses import dataclass
from datetime import datetime, timezone
@@ -5,15 +6,14 @@ from decimal import Decimal
from enum import Enum
from math import isinf, isnan
from pathlib import PurePath, PurePosixPath, PureWindowsPath
from typing import Optional
from typing import Optional, TypedDict
import pytest
from fastapi._compat import Undefined
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import PydanticV1NotSupportedError
from pydantic import BaseModel, Field, ValidationError
from .utils import needs_pydanticv1
class Person:
def __init__(self, name: str):
@@ -156,29 +156,17 @@ def test_encode_custom_json_encoders_model_pydanticv2():
assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
# TODO: remove when deprecating Pydantic v1
@needs_pydanticv1
def test_encode_custom_json_encoders_model_pydanticv1():
from pydantic import v1
def test_json_encoder_error_with_pydanticv1():
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
from pydantic import v1
class ModelWithCustomEncoder(v1.BaseModel):
dt_field: datetime
class ModelV1(v1.BaseModel):
name: str
class Config:
json_encoders = {
datetime: lambda dt: dt.replace(
microsecond=0, tzinfo=timezone.utc
).isoformat()
}
class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder):
class Config:
pass
model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8))
assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8))
assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
data = ModelV1(name="test")
with pytest.raises(PydanticV1NotSupportedError):
jsonable_encoder(data)
def test_encode_model_with_config():
@@ -214,25 +202,27 @@ def test_encode_model_with_default():
}
@needs_pydanticv1
def test_custom_encoders():
from pydantic import v1
class safe_datetime(datetime):
pass
class MyModel(v1.BaseModel):
class MyDict(TypedDict):
dt_field: safe_datetime
instance = MyModel(dt_field=safe_datetime.now())
instance = MyDict(dt_field=safe_datetime.now())
encoded_instance = jsonable_encoder(
instance, custom_encoder={safe_datetime: lambda o: o.strftime("%H:%M:%S")}
)
assert encoded_instance["dt_field"] == instance.dt_field.strftime("%H:%M:%S")
assert encoded_instance["dt_field"] == instance["dt_field"].strftime("%H:%M:%S")
encoded_instance = jsonable_encoder(
instance, custom_encoder={datetime: lambda o: o.strftime("%H:%M:%S")}
)
assert encoded_instance["dt_field"] == instance["dt_field"].strftime("%H:%M:%S")
encoded_instance2 = jsonable_encoder(instance)
assert encoded_instance2["dt_field"] == instance.dt_field.isoformat()
assert encoded_instance2["dt_field"] == instance["dt_field"].isoformat()
def test_custom_enum_encoders():
@@ -287,17 +277,6 @@ def test_encode_pure_path():
assert jsonable_encoder({"path": test_path}) == {"path": str(test_path)}
@needs_pydanticv1
def test_encode_root():
from pydantic import v1
class ModelWithRoot(v1.BaseModel):
__root__: str
model = ModelWithRoot(__root__="Foo")
assert jsonable_encoder(model) == "Foo"
def test_decimal_encoder_float():
data = {"value": Decimal(1.23)}
assert jsonable_encoder(data) == {"value": 1.23}

View File

@@ -1,99 +0,0 @@
import sys
import pytest
from fastapi.exceptions import FastAPIDeprecationWarning
from tests.utils import skip_module_if_py_gte_314
if sys.version_info >= (3, 14):
skip_module_if_py_gte_314()
from fastapi import FastAPI
from fastapi._compat.v1 import BaseModel
from fastapi.testclient import TestClient
def test_warns_pydantic_v1_model_in_endpoint_param() -> None:
class ParamModelV1(BaseModel):
name: str
app = FastAPI()
with pytest.warns(
FastAPIDeprecationWarning,
match=r"pydantic\.v1 is deprecated.*Please update the param data:",
):
@app.post("/param")
def endpoint(data: ParamModelV1):
return data
client = TestClient(app)
response = client.post("/param", json={"name": "test"})
assert response.status_code == 200, response.text
assert response.json() == {"name": "test"}
def test_warns_pydantic_v1_model_in_return_type() -> None:
class ReturnModelV1(BaseModel):
name: str
app = FastAPI()
with pytest.warns(
FastAPIDeprecationWarning,
match=r"pydantic\.v1 is deprecated.*Please update the response model",
):
@app.get("/return")
def endpoint() -> ReturnModelV1:
return ReturnModelV1(name="test")
client = TestClient(app)
response = client.get("/return")
assert response.status_code == 200, response.text
assert response.json() == {"name": "test"}
def test_warns_pydantic_v1_model_in_response_model() -> None:
class ResponseModelV1(BaseModel):
name: str
app = FastAPI()
with pytest.warns(
FastAPIDeprecationWarning,
match=r"pydantic\.v1 is deprecated.*Please update the response model",
):
@app.get("/response-model", response_model=ResponseModelV1)
def endpoint():
return {"name": "test"}
client = TestClient(app)
response = client.get("/response-model")
assert response.status_code == 200, response.text
assert response.json() == {"name": "test"}
def test_warns_pydantic_v1_model_in_additional_responses_model() -> None:
class ErrorModelV1(BaseModel):
detail: str
app = FastAPI()
with pytest.warns(
FastAPIDeprecationWarning,
match=r"pydantic\.v1 is deprecated.*In responses=\{\}, please update",
):
@app.get(
"/responses", response_model=None, responses={400: {"model": ErrorModelV1}}
)
def endpoint():
return {"ok": True}
client = TestClient(app)
response = client.get("/responses")
assert response.status_code == 200, response.text
assert response.json() == {"ok": True}

View File

@@ -0,0 +1,70 @@
import sys
import warnings
import pytest
from tests.utils import skip_module_if_py_gte_314
if sys.version_info >= (3, 14):
skip_module_if_py_gte_314()
from fastapi import FastAPI
from fastapi.exceptions import PydanticV1NotSupportedError
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
from pydantic.v1 import BaseModel
def test_raises_pydantic_v1_model_in_endpoint_param() -> None:
class ParamModelV1(BaseModel):
name: str
app = FastAPI()
with pytest.raises(PydanticV1NotSupportedError):
@app.post("/param")
def endpoint(data: ParamModelV1): # pragma: no cover
return data
def test_raises_pydantic_v1_model_in_return_type() -> None:
class ReturnModelV1(BaseModel):
name: str
app = FastAPI()
with pytest.raises(PydanticV1NotSupportedError):
@app.get("/return")
def endpoint() -> ReturnModelV1: # pragma: no cover
return ReturnModelV1(name="test")
def test_raises_pydantic_v1_model_in_response_model() -> None:
class ResponseModelV1(BaseModel):
name: str
app = FastAPI()
with pytest.raises(PydanticV1NotSupportedError):
@app.get("/response-model", response_model=ResponseModelV1)
def endpoint(): # pragma: no cover
return {"name": "test"}
def test_raises_pydantic_v1_model_in_additional_responses_model() -> None:
class ErrorModelV1(BaseModel):
detail: str
app = FastAPI()
with pytest.raises(PydanticV1NotSupportedError):
@app.get(
"/responses", response_model=None, responses={400: {"model": ErrorModelV1}}
)
def endpoint(): # pragma: no cover
return {"ok": True}

View File

@@ -1,439 +0,0 @@
import sys
import warnings
from typing import Any, Union
from tests.utils import skip_module_if_py_gte_314
if sys.version_info >= (3, 14):
skip_module_if_py_gte_314()
from fastapi import FastAPI
from fastapi._compat.v1 import BaseModel
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
class SubItem(BaseModel):
name: str
class Item(BaseModel):
title: str
size: int
description: Union[str, None] = None
sub: SubItem
multi: list[SubItem] = []
app = FastAPI()
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.post("/simple-model")
def handle_simple_model(data: SubItem) -> SubItem:
return data
@app.post("/simple-model-filter", response_model=SubItem)
def handle_simple_model_filter(data: SubItem) -> Any:
extended_data = data.dict()
extended_data.update({"secret_price": 42})
return extended_data
@app.post("/item")
def handle_item(data: Item) -> Item:
return data
@app.post("/item-filter", response_model=Item)
def handle_item_filter(data: Item) -> Any:
extended_data = data.dict()
extended_data.update({"secret_data": "classified", "internal_id": 12345})
extended_data["sub"].update({"internal_id": 67890})
return extended_data
client = TestClient(app)
def test_old_simple_model():
response = client.post(
"/simple-model",
json={"name": "Foo"},
)
assert response.status_code == 200, response.text
assert response.json() == {"name": "Foo"}
def test_old_simple_model_validation_error():
response = client.post(
"/simple-model",
json={"wrong_name": "Foo"},
)
assert response.status_code == 422, response.text
assert response.json() == snapshot(
{
"detail": [
{
"loc": ["body", "name"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_old_simple_model_filter():
response = client.post(
"/simple-model-filter",
json={"name": "Foo"},
)
assert response.status_code == 200, response.text
assert response.json() == {"name": "Foo"}
def test_item_model():
response = client.post(
"/item",
json={
"title": "Test Item",
"size": 100,
"description": "This is a test item",
"sub": {"name": "SubItem1"},
"multi": [{"name": "Multi1"}, {"name": "Multi2"}],
},
)
assert response.status_code == 200, response.text
assert response.json() == {
"title": "Test Item",
"size": 100,
"description": "This is a test item",
"sub": {"name": "SubItem1"},
"multi": [{"name": "Multi1"}, {"name": "Multi2"}],
}
def test_item_model_minimal():
response = client.post(
"/item",
json={"title": "Minimal Item", "size": 50, "sub": {"name": "SubMin"}},
)
assert response.status_code == 200, response.text
assert response.json() == {
"title": "Minimal Item",
"size": 50,
"description": None,
"sub": {"name": "SubMin"},
"multi": [],
}
def test_item_model_validation_errors():
response = client.post(
"/item",
json={"title": "Missing fields"},
)
assert response.status_code == 422, response.text
error_detail = response.json()["detail"]
assert len(error_detail) == 2
assert {
"loc": ["body", "size"],
"msg": "field required",
"type": "value_error.missing",
} in error_detail
assert {
"loc": ["body", "sub"],
"msg": "field required",
"type": "value_error.missing",
} in error_detail
def test_item_model_nested_validation_error():
response = client.post(
"/item",
json={"title": "Test Item", "size": 100, "sub": {"wrong_field": "test"}},
)
assert response.status_code == 422, response.text
assert response.json() == snapshot(
{
"detail": [
{
"loc": ["body", "sub", "name"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_item_model_invalid_type():
response = client.post(
"/item",
json={"title": "Test Item", "size": "not_a_number", "sub": {"name": "SubItem"}},
)
assert response.status_code == 422, response.text
assert response.json() == snapshot(
{
"detail": [
{
"loc": ["body", "size"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
def test_item_filter():
response = client.post(
"/item-filter",
json={
"title": "Filtered Item",
"size": 200,
"description": "Test filtering",
"sub": {"name": "SubFiltered"},
"multi": [],
},
)
assert response.status_code == 200, response.text
result = response.json()
assert result == {
"title": "Filtered Item",
"size": 200,
"description": "Test filtering",
"sub": {"name": "SubFiltered"},
"multi": [],
}
assert "secret_data" not in result
assert "internal_id" not in result
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/simple-model": {
"post": {
"summary": "Handle Simple Model",
"operationId": "handle_simple_model_simple_model_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"allOf": [
{"$ref": "#/components/schemas/SubItem"}
],
"title": "Data",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SubItem"
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/simple-model-filter": {
"post": {
"summary": "Handle Simple Model Filter",
"operationId": "handle_simple_model_filter_simple_model_filter_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"allOf": [
{"$ref": "#/components/schemas/SubItem"}
],
"title": "Data",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SubItem"
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/item": {
"post": {
"summary": "Handle Item",
"operationId": "handle_item_item_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"allOf": [
{"$ref": "#/components/schemas/Item"}
],
"title": "Data",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/item-filter": {
"post": {
"summary": "Handle Item Filter",
"operationId": "handle_item_filter_item_filter_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"allOf": [
{"$ref": "#/components/schemas/Item"}
],
"title": "Data",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
},
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail",
}
},
"type": "object",
"title": "HTTPValidationError",
},
"Item": {
"properties": {
"title": {"type": "string", "title": "Title"},
"size": {"type": "integer", "title": "Size"},
"description": {"type": "string", "title": "Description"},
"sub": {"$ref": "#/components/schemas/SubItem"},
"multi": {
"items": {"$ref": "#/components/schemas/SubItem"},
"type": "array",
"title": "Multi",
"default": [],
},
},
"type": "object",
"required": ["title", "size", "sub"],
"title": "Item",
},
"SubItem": {
"properties": {"name": {"type": "string", "title": "Name"}},
"type": "object",
"required": ["name"],
"title": "SubItem",
},
"ValidationError": {
"properties": {
"loc": {
"items": {
"anyOf": [{"type": "string"}, {"type": "integer"}]
},
"type": "array",
"title": "Location",
},
"msg": {"type": "string", "title": "Message"},
"type": {"type": "string", "title": "Error Type"},
},
"type": "object",
"required": ["loc", "msg", "type"],
"title": "ValidationError",
},
}
},
}
)

View File

@@ -1,682 +0,0 @@
import sys
import warnings
from typing import Any, Union
from tests.utils import skip_module_if_py_gte_314
if sys.version_info >= (3, 14):
skip_module_if_py_gte_314()
from fastapi import FastAPI
from fastapi._compat.v1 import BaseModel
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
class SubItem(BaseModel):
name: str
class Item(BaseModel):
title: str
size: int
description: Union[str, None] = None
sub: SubItem
multi: list[SubItem] = []
app = FastAPI()
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.post("/item")
def handle_item(data: Item) -> list[Item]:
return [data, data]
@app.post("/item-filter", response_model=list[Item])
def handle_item_filter(data: Item) -> Any:
extended_data = data.dict()
extended_data.update({"secret_data": "classified", "internal_id": 12345})
extended_data["sub"].update({"internal_id": 67890})
return [extended_data, extended_data]
@app.post("/item-list")
def handle_item_list(data: list[Item]) -> Item:
if data:
return data[0]
return Item(title="", size=0, sub=SubItem(name=""))
@app.post("/item-list-filter", response_model=Item)
def handle_item_list_filter(data: list[Item]) -> Any:
if data:
extended_data = data[0].dict()
extended_data.update({"secret_data": "classified", "internal_id": 12345})
extended_data["sub"].update({"internal_id": 67890})
return extended_data
return Item(title="", size=0, sub=SubItem(name=""))
@app.post("/item-list-to-list")
def handle_item_list_to_list(data: list[Item]) -> list[Item]:
return data
@app.post("/item-list-to-list-filter", response_model=list[Item])
def handle_item_list_to_list_filter(data: list[Item]) -> Any:
if data:
extended_data = data[0].dict()
extended_data.update({"secret_data": "classified", "internal_id": 12345})
extended_data["sub"].update({"internal_id": 67890})
return [extended_data, extended_data]
return []
client = TestClient(app)
def test_item_to_list():
response = client.post(
"/item",
json={
"title": "Test Item",
"size": 100,
"description": "This is a test item",
"sub": {"name": "SubItem1"},
"multi": [{"name": "Multi1"}, {"name": "Multi2"}],
},
)
assert response.status_code == 200, response.text
result = response.json()
assert isinstance(result, list)
assert len(result) == 2
for item in result:
assert item == {
"title": "Test Item",
"size": 100,
"description": "This is a test item",
"sub": {"name": "SubItem1"},
"multi": [{"name": "Multi1"}, {"name": "Multi2"}],
}
def test_item_to_list_filter():
response = client.post(
"/item-filter",
json={
"title": "Filtered Item",
"size": 200,
"description": "Test filtering",
"sub": {"name": "SubFiltered"},
"multi": [],
},
)
assert response.status_code == 200, response.text
result = response.json()
assert isinstance(result, list)
assert len(result) == 2
for item in result:
assert item == {
"title": "Filtered Item",
"size": 200,
"description": "Test filtering",
"sub": {"name": "SubFiltered"},
"multi": [],
}
# Verify secret fields are filtered out
assert "secret_data" not in item
assert "internal_id" not in item
assert "internal_id" not in item["sub"]
def test_list_to_item():
response = client.post(
"/item-list",
json=[
{"title": "First Item", "size": 50, "sub": {"name": "First Sub"}},
{"title": "Second Item", "size": 75, "sub": {"name": "Second Sub"}},
],
)
assert response.status_code == 200, response.text
assert response.json() == {
"title": "First Item",
"size": 50,
"description": None,
"sub": {"name": "First Sub"},
"multi": [],
}
def test_list_to_item_empty():
response = client.post(
"/item-list",
json=[],
)
assert response.status_code == 200, response.text
assert response.json() == {
"title": "",
"size": 0,
"description": None,
"sub": {"name": ""},
"multi": [],
}
def test_list_to_item_filter():
response = client.post(
"/item-list-filter",
json=[
{
"title": "First Item",
"size": 100,
"sub": {"name": "First Sub"},
"multi": [{"name": "Multi1"}],
},
{"title": "Second Item", "size": 200, "sub": {"name": "Second Sub"}},
],
)
assert response.status_code == 200, response.text
result = response.json()
assert result == {
"title": "First Item",
"size": 100,
"description": None,
"sub": {"name": "First Sub"},
"multi": [{"name": "Multi1"}],
}
# Verify secret fields are filtered out
assert "secret_data" not in result
assert "internal_id" not in result
def test_list_to_item_filter_no_data():
response = client.post("/item-list-filter", json=[])
assert response.status_code == 200, response.text
assert response.json() == {
"title": "",
"size": 0,
"description": None,
"sub": {"name": ""},
"multi": [],
}
def test_list_to_list():
input_items = [
{"title": "Item 1", "size": 10, "sub": {"name": "Sub1"}},
{
"title": "Item 2",
"size": 20,
"description": "Second item",
"sub": {"name": "Sub2"},
"multi": [{"name": "M1"}, {"name": "M2"}],
},
{"title": "Item 3", "size": 30, "sub": {"name": "Sub3"}},
]
response = client.post(
"/item-list-to-list",
json=input_items,
)
assert response.status_code == 200, response.text
result = response.json()
assert isinstance(result, list)
assert len(result) == 3
assert result[0] == {
"title": "Item 1",
"size": 10,
"description": None,
"sub": {"name": "Sub1"},
"multi": [],
}
assert result[1] == {
"title": "Item 2",
"size": 20,
"description": "Second item",
"sub": {"name": "Sub2"},
"multi": [{"name": "M1"}, {"name": "M2"}],
}
assert result[2] == {
"title": "Item 3",
"size": 30,
"description": None,
"sub": {"name": "Sub3"},
"multi": [],
}
def test_list_to_list_filter():
response = client.post(
"/item-list-to-list-filter",
json=[{"title": "Item 1", "size": 100, "sub": {"name": "Sub1"}}],
)
assert response.status_code == 200, response.text
result = response.json()
assert isinstance(result, list)
assert len(result) == 2
for item in result:
assert item == {
"title": "Item 1",
"size": 100,
"description": None,
"sub": {"name": "Sub1"},
"multi": [],
}
# Verify secret fields are filtered out
assert "secret_data" not in item
assert "internal_id" not in item
def test_list_to_list_filter_no_data():
response = client.post(
"/item-list-to-list-filter",
json=[],
)
assert response.status_code == 200, response.text
assert response.json() == []
def test_list_validation_error():
response = client.post(
"/item-list",
json=[
{"title": "Valid Item", "size": 100, "sub": {"name": "Sub1"}},
{
"title": "Invalid Item"
# Missing required fields: size and sub
},
],
)
assert response.status_code == 422, response.text
error_detail = response.json()["detail"]
assert len(error_detail) == 2
assert {
"loc": ["body", 1, "size"],
"msg": "field required",
"type": "value_error.missing",
} in error_detail
assert {
"loc": ["body", 1, "sub"],
"msg": "field required",
"type": "value_error.missing",
} in error_detail
def test_list_nested_validation_error():
response = client.post(
"/item-list",
json=[
{"title": "Item with bad sub", "size": 100, "sub": {"wrong_field": "value"}}
],
)
assert response.status_code == 422, response.text
assert response.json() == snapshot(
{
"detail": [
{
"loc": ["body", 0, "sub", "name"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_list_type_validation_error():
response = client.post(
"/item-list",
json=[{"title": "Item", "size": "not_a_number", "sub": {"name": "Sub"}}],
)
assert response.status_code == 422, response.text
assert response.json() == snapshot(
{
"detail": [
{
"loc": ["body", 0, "size"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
def test_invalid_list_structure():
response = client.post(
"/item-list",
json={"title": "Not a list", "size": 100, "sub": {"name": "Sub"}},
)
assert response.status_code == 422, response.text
assert response.json() == snapshot(
{
"detail": [
{
"loc": ["body"],
"msg": "value is not a valid list",
"type": "type_error.list",
}
]
}
)
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/item": {
"post": {
"summary": "Handle Item",
"operationId": "handle_item_item_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"allOf": [
{"$ref": "#/components/schemas/Item"}
],
"title": "Data",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/Item"
},
"type": "array",
"title": "Response Handle Item Item Post",
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/item-filter": {
"post": {
"summary": "Handle Item Filter",
"operationId": "handle_item_filter_item_filter_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"allOf": [
{"$ref": "#/components/schemas/Item"}
],
"title": "Data",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/Item"
},
"type": "array",
"title": "Response Handle Item Filter Item Filter Post",
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/item-list": {
"post": {
"summary": "Handle Item List",
"operationId": "handle_item_list_item_list_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"items": {"$ref": "#/components/schemas/Item"},
"type": "array",
"title": "Data",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/item-list-filter": {
"post": {
"summary": "Handle Item List Filter",
"operationId": "handle_item_list_filter_item_list_filter_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"items": {"$ref": "#/components/schemas/Item"},
"type": "array",
"title": "Data",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/item-list-to-list": {
"post": {
"summary": "Handle Item List To List",
"operationId": "handle_item_list_to_list_item_list_to_list_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"items": {"$ref": "#/components/schemas/Item"},
"type": "array",
"title": "Data",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/Item"
},
"type": "array",
"title": "Response Handle Item List To List Item List To List Post",
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/item-list-to-list-filter": {
"post": {
"summary": "Handle Item List To List Filter",
"operationId": "handle_item_list_to_list_filter_item_list_to_list_filter_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"items": {"$ref": "#/components/schemas/Item"},
"type": "array",
"title": "Data",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/Item"
},
"type": "array",
"title": "Response Handle Item List To List Filter Item List To List Filter Post",
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
},
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail",
}
},
"type": "object",
"title": "HTTPValidationError",
},
"Item": {
"properties": {
"title": {"type": "string", "title": "Title"},
"size": {"type": "integer", "title": "Size"},
"description": {"type": "string", "title": "Description"},
"sub": {"$ref": "#/components/schemas/SubItem"},
"multi": {
"items": {"$ref": "#/components/schemas/SubItem"},
"type": "array",
"title": "Multi",
"default": [],
},
},
"type": "object",
"required": ["title", "size", "sub"],
"title": "Item",
},
"SubItem": {
"properties": {"name": {"type": "string", "title": "Name"}},
"type": "object",
"required": ["name"],
"title": "SubItem",
},
"ValidationError": {
"properties": {
"loc": {
"items": {
"anyOf": [{"type": "string"}, {"type": "integer"}]
},
"type": "array",
"title": "Location",
},
"msg": {"type": "string", "title": "Message"},
"type": {"type": "string", "title": "Error Type"},
},
"type": "object",
"required": ["loc", "msg", "type"],
"title": "ValidationError",
},
}
},
}
)

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,137 +0,0 @@
import warnings
from fastapi import FastAPI
from . import modelsv1, modelsv2, modelsv2b
app = FastAPI()
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.post("/v1-to-v2/item")
def handle_v1_item_to_v2(data: modelsv1.Item) -> modelsv2.Item:
return modelsv2.Item(
new_title=data.title,
new_size=data.size,
new_description=data.description,
new_sub=modelsv2.SubItem(new_sub_name=data.sub.name),
new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi],
)
@app.post("/v2-to-v1/item")
def handle_v2_item_to_v1(data: modelsv2.Item) -> modelsv1.Item:
return modelsv1.Item(
title=data.new_title,
size=data.new_size,
description=data.new_description,
sub=modelsv1.SubItem(name=data.new_sub.new_sub_name),
multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi],
)
@app.post("/v1-to-v2/item-to-list")
def handle_v1_item_to_v2_list(data: modelsv1.Item) -> list[modelsv2.Item]:
converted = modelsv2.Item(
new_title=data.title,
new_size=data.size,
new_description=data.description,
new_sub=modelsv2.SubItem(new_sub_name=data.sub.name),
new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi],
)
return [converted, converted]
@app.post("/v1-to-v2/list-to-list")
def handle_v1_list_to_v2_list(data: list[modelsv1.Item]) -> list[modelsv2.Item]:
result = []
for item in data:
result.append(
modelsv2.Item(
new_title=item.title,
new_size=item.size,
new_description=item.description,
new_sub=modelsv2.SubItem(new_sub_name=item.sub.name),
new_multi=[
modelsv2.SubItem(new_sub_name=s.name) for s in item.multi
],
)
)
return result
@app.post("/v1-to-v2/list-to-item")
def handle_v1_list_to_v2_item(data: list[modelsv1.Item]) -> modelsv2.Item:
if data:
item = data[0]
return modelsv2.Item(
new_title=item.title,
new_size=item.size,
new_description=item.description,
new_sub=modelsv2.SubItem(new_sub_name=item.sub.name),
new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in item.multi],
)
return modelsv2.Item(
new_title="", new_size=0, new_sub=modelsv2.SubItem(new_sub_name="")
)
@app.post("/v2-to-v1/item-to-list")
def handle_v2_item_to_v1_list(data: modelsv2.Item) -> list[modelsv1.Item]:
converted = modelsv1.Item(
title=data.new_title,
size=data.new_size,
description=data.new_description,
sub=modelsv1.SubItem(name=data.new_sub.new_sub_name),
multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi],
)
return [converted, converted]
@app.post("/v2-to-v1/list-to-list")
def handle_v2_list_to_v1_list(data: list[modelsv2.Item]) -> list[modelsv1.Item]:
result = []
for item in data:
result.append(
modelsv1.Item(
title=item.new_title,
size=item.new_size,
description=item.new_description,
sub=modelsv1.SubItem(name=item.new_sub.new_sub_name),
multi=[
modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi
],
)
)
return result
@app.post("/v2-to-v1/list-to-item")
def handle_v2_list_to_v1_item(data: list[modelsv2.Item]) -> modelsv1.Item:
if data:
item = data[0]
return modelsv1.Item(
title=item.new_title,
size=item.new_size,
description=item.new_description,
sub=modelsv1.SubItem(name=item.new_sub.new_sub_name),
multi=[modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi],
)
return modelsv1.Item(title="", size=0, sub=modelsv1.SubItem(name=""))
@app.post("/v2-to-v1/same-name")
def handle_v2_same_name_to_v1(
item1: modelsv2.Item, item2: modelsv2b.Item
) -> modelsv1.Item:
return modelsv1.Item(
title=item1.new_title,
size=item2.dup_size,
description=item1.new_description,
sub=modelsv1.SubItem(name=item1.new_sub.new_sub_name),
multi=[modelsv1.SubItem(name=s.dup_sub_name) for s in item2.dup_multi],
)
@app.post("/v2-to-v1/list-of-items-to-list-of-items")
def handle_v2_items_in_list_to_v1_item_in_list(
data1: list[modelsv2.ItemInList], data2: list[modelsv2b.ItemInList]
) -> list[modelsv1.ItemInList]:
item1 = data1[0]
item2 = data2[0]
return [
modelsv1.ItemInList(name1=item1.name2),
modelsv1.ItemInList(name1=item2.dup_name2),
]

View File

@@ -1,19 +0,0 @@
from typing import Union
from fastapi._compat.v1 import BaseModel
class SubItem(BaseModel):
name: str
class Item(BaseModel):
title: str
size: int
description: Union[str, None] = None
sub: SubItem
multi: list[SubItem] = []
class ItemInList(BaseModel):
name1: str

View File

@@ -1,19 +0,0 @@
from typing import Union
from pydantic import BaseModel
class SubItem(BaseModel):
new_sub_name: str
class Item(BaseModel):
new_title: str
new_size: int
new_description: Union[str, None] = None
new_sub: SubItem
new_multi: list[SubItem] = []
class ItemInList(BaseModel):
name2: str

View File

@@ -1,19 +0,0 @@
from typing import Union
from pydantic import BaseModel
class SubItem(BaseModel):
dup_sub_name: str
class Item(BaseModel):
dup_title: str
dup_size: int
dup_description: Union[str, None] = None
dup_sub: SubItem
dup_multi: list[SubItem] = []
class ItemInList(BaseModel):
dup_name2: str

View File

@@ -1,951 +0,0 @@
import sys
from tests.utils import skip_module_if_py_gte_314
if sys.version_info >= (3, 14):
skip_module_if_py_gte_314()
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from .main import app
client = TestClient(app)
def test_v1_to_v2_item():
response = client.post(
"/v1-to-v2/item",
json={"title": "Test", "size": 10, "sub": {"name": "SubTest"}},
)
assert response.status_code == 200
assert response.json() == {
"new_title": "Test",
"new_size": 10,
"new_description": None,
"new_sub": {"new_sub_name": "SubTest"},
"new_multi": [],
}
def test_v2_to_v1_item():
response = client.post(
"/v2-to-v1/item",
json={
"new_title": "NewTest",
"new_size": 20,
"new_sub": {"new_sub_name": "NewSubTest"},
},
)
assert response.status_code == 200
assert response.json() == {
"title": "NewTest",
"size": 20,
"description": None,
"sub": {"name": "NewSubTest"},
"multi": [],
}
def test_v1_to_v2_item_to_list():
response = client.post(
"/v1-to-v2/item-to-list",
json={"title": "ListTest", "size": 30, "sub": {"name": "SubListTest"}},
)
assert response.status_code == 200
assert response.json() == [
{
"new_title": "ListTest",
"new_size": 30,
"new_description": None,
"new_sub": {"new_sub_name": "SubListTest"},
"new_multi": [],
},
{
"new_title": "ListTest",
"new_size": 30,
"new_description": None,
"new_sub": {"new_sub_name": "SubListTest"},
"new_multi": [],
},
]
def test_v1_to_v2_list_to_list():
response = client.post(
"/v1-to-v2/list-to-list",
json=[
{"title": "Item1", "size": 40, "sub": {"name": "Sub1"}},
{"title": "Item2", "size": 50, "sub": {"name": "Sub2"}},
],
)
assert response.status_code == 200
assert response.json() == [
{
"new_title": "Item1",
"new_size": 40,
"new_description": None,
"new_sub": {"new_sub_name": "Sub1"},
"new_multi": [],
},
{
"new_title": "Item2",
"new_size": 50,
"new_description": None,
"new_sub": {"new_sub_name": "Sub2"},
"new_multi": [],
},
]
def test_v1_to_v2_list_to_item():
response = client.post(
"/v1-to-v2/list-to-item",
json=[
{"title": "FirstItem", "size": 60, "sub": {"name": "FirstSub"}},
{"title": "SecondItem", "size": 70, "sub": {"name": "SecondSub"}},
],
)
assert response.status_code == 200
assert response.json() == {
"new_title": "FirstItem",
"new_size": 60,
"new_description": None,
"new_sub": {"new_sub_name": "FirstSub"},
"new_multi": [],
}
def test_v2_to_v1_item_to_list():
response = client.post(
"/v2-to-v1/item-to-list",
json={
"new_title": "ListNew",
"new_size": 80,
"new_sub": {"new_sub_name": "SubListNew"},
},
)
assert response.status_code == 200
assert response.json() == [
{
"title": "ListNew",
"size": 80,
"description": None,
"sub": {"name": "SubListNew"},
"multi": [],
},
{
"title": "ListNew",
"size": 80,
"description": None,
"sub": {"name": "SubListNew"},
"multi": [],
},
]
def test_v2_to_v1_list_to_list():
response = client.post(
"/v2-to-v1/list-to-list",
json=[
{
"new_title": "New1",
"new_size": 90,
"new_sub": {"new_sub_name": "NewSub1"},
},
{
"new_title": "New2",
"new_size": 100,
"new_sub": {"new_sub_name": "NewSub2"},
},
],
)
assert response.status_code == 200
assert response.json() == [
{
"title": "New1",
"size": 90,
"description": None,
"sub": {"name": "NewSub1"},
"multi": [],
},
{
"title": "New2",
"size": 100,
"description": None,
"sub": {"name": "NewSub2"},
"multi": [],
},
]
def test_v2_to_v1_list_to_item():
response = client.post(
"/v2-to-v1/list-to-item",
json=[
{
"new_title": "FirstNew",
"new_size": 110,
"new_sub": {"new_sub_name": "FirstNewSub"},
},
{
"new_title": "SecondNew",
"new_size": 120,
"new_sub": {"new_sub_name": "SecondNewSub"},
},
],
)
assert response.status_code == 200
assert response.json() == {
"title": "FirstNew",
"size": 110,
"description": None,
"sub": {"name": "FirstNewSub"},
"multi": [],
}
def test_v1_to_v2_list_to_item_empty():
response = client.post("/v1-to-v2/list-to-item", json=[])
assert response.status_code == 200
assert response.json() == {
"new_title": "",
"new_size": 0,
"new_description": None,
"new_sub": {"new_sub_name": ""},
"new_multi": [],
}
def test_v2_to_v1_list_to_item_empty():
response = client.post("/v2-to-v1/list-to-item", json=[])
assert response.status_code == 200
assert response.json() == {
"title": "",
"size": 0,
"description": None,
"sub": {"name": ""},
"multi": [],
}
def test_v2_same_name_to_v1():
response = client.post(
"/v2-to-v1/same-name",
json={
"item1": {
"new_title": "Title1",
"new_size": 100,
"new_description": "Description1",
"new_sub": {"new_sub_name": "Sub1"},
"new_multi": [{"new_sub_name": "Multi1"}],
},
"item2": {
"dup_title": "Title2",
"dup_size": 200,
"dup_description": "Description2",
"dup_sub": {"dup_sub_name": "Sub2"},
"dup_multi": [
{"dup_sub_name": "Multi2a"},
{"dup_sub_name": "Multi2b"},
],
},
},
)
assert response.status_code == 200
assert response.json() == {
"title": "Title1",
"size": 200,
"description": "Description1",
"sub": {"name": "Sub1"},
"multi": [{"name": "Multi2a"}, {"name": "Multi2b"}],
}
def test_v2_items_in_list_to_v1_item_in_list():
response = client.post(
"/v2-to-v1/list-of-items-to-list-of-items",
json={
"data1": [{"name2": "Item1"}, {"name2": "Item2"}],
"data2": [{"dup_name2": "Item3"}, {"dup_name2": "Item4"}],
},
)
assert response.status_code == 200, response.text
assert response.json() == [
{"name1": "Item1"},
{"name1": "Item3"},
]
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/v1-to-v2/item": {
"post": {
"summary": "Handle V1 Item To V2",
"operationId": "handle_v1_item_to_v2_v1_to_v2_item_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"allOf": [
{
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
}
],
"title": "Data",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item"
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/v2-to-v1/item": {
"post": {
"summary": "Handle V2 Item To V1",
"operationId": "handle_v2_item_to_v1_v2_to_v1_item_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input"
},
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/v1-to-v2/item-to-list": {
"post": {
"summary": "Handle V1 Item To V2 List",
"operationId": "handle_v1_item_to_v2_list_v1_to_v2_item_to_list_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"allOf": [
{
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
}
],
"title": "Data",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item"
},
"type": "array",
"title": "Response Handle V1 Item To V2 List V1 To V2 Item To List Post",
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/v1-to-v2/list-to-list": {
"post": {
"summary": "Handle V1 List To V2 List",
"operationId": "handle_v1_list_to_v2_list_v1_to_v2_list_to_list_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
},
"type": "array",
"title": "Data",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item"
},
"type": "array",
"title": "Response Handle V1 List To V2 List V1 To V2 List To List Post",
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/v1-to-v2/list-to-item": {
"post": {
"summary": "Handle V1 List To V2 Item",
"operationId": "handle_v1_list_to_v2_item_v1_to_v2_list_to_item_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
},
"type": "array",
"title": "Data",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item"
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/v2-to-v1/item-to-list": {
"post": {
"summary": "Handle V2 Item To V1 List",
"operationId": "handle_v2_item_to_v1_list_v2_to_v1_item_to_list_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input"
},
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
},
"type": "array",
"title": "Response Handle V2 Item To V1 List V2 To V1 Item To List Post",
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/v2-to-v1/list-to-list": {
"post": {
"summary": "Handle V2 List To V1 List",
"operationId": "handle_v2_list_to_v1_list_v2_to_v1_list_to_list_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input"
},
"type": "array",
"title": "Data",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
},
"type": "array",
"title": "Response Handle V2 List To V1 List V2 To V1 List To List Post",
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/v2-to-v1/list-to-item": {
"post": {
"summary": "Handle V2 List To V1 Item",
"operationId": "handle_v2_list_to_v1_item_v2_to_v1_list_to_item_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input"
},
"type": "array",
"title": "Data",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/v2-to-v1/same-name": {
"post": {
"summary": "Handle V2 Same Name To V1",
"operationId": "handle_v2_same_name_to_v1_v2_to_v1_same_name_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post"
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/v2-to-v1/list-of-items-to-list-of-items": {
"post": {
"summary": "Handle V2 Items In List To V1 Item In List",
"operationId": "handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post"
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList"
},
"type": "array",
"title": "Response Handle V2 Items In List To V1 Item In List V2 To V1 List Of Items To List Of Items Post",
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
},
"components": {
"schemas": {
"Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post": {
"properties": {
"data1": {
"items": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList"
},
"type": "array",
"title": "Data1",
},
"data2": {
"items": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList"
},
"type": "array",
"title": "Data2",
},
},
"type": "object",
"required": ["data1", "data2"],
"title": "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post",
},
"Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post": {
"properties": {
"item1": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input"
},
"item2": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__Item"
},
},
"type": "object",
"required": ["item1", "item2"],
"title": "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post",
},
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail",
}
},
"type": "object",
"title": "HTTPValidationError",
},
"ValidationError": {
"properties": {
"loc": {
"items": {
"anyOf": [
{"type": "string"},
{"type": "integer"},
]
},
"type": "array",
"title": "Location",
},
"msg": {"type": "string", "title": "Message"},
"type": {"type": "string", "title": "Error Type"},
},
"type": "object",
"required": ["loc", "msg", "type"],
"title": "ValidationError",
},
"tests__test_pydantic_v1_v2_multifile__modelsv1__Item": {
"properties": {
"title": {"type": "string", "title": "Title"},
"size": {"type": "integer", "title": "Size"},
"description": {
"type": "string",
"title": "Description",
},
"sub": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem"
},
"multi": {
"items": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem"
},
"type": "array",
"title": "Multi",
"default": [],
},
},
"type": "object",
"required": ["title", "size", "sub"],
"title": "Item",
},
"tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList": {
"properties": {"name1": {"type": "string", "title": "Name1"}},
"type": "object",
"required": ["name1"],
"title": "ItemInList",
},
"tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem": {
"properties": {"name": {"type": "string", "title": "Name"}},
"type": "object",
"required": ["name"],
"title": "SubItem",
},
"tests__test_pydantic_v1_v2_multifile__modelsv2__Item": {
"properties": {
"new_title": {
"type": "string",
"title": "New Title",
},
"new_size": {
"type": "integer",
"title": "New Size",
},
"new_description": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "New Description",
},
"new_sub": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem"
},
"new_multi": {
"items": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem"
},
"type": "array",
"title": "New Multi",
"default": [],
},
},
"type": "object",
"required": ["new_title", "new_size", "new_sub"],
"title": "Item",
},
"tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input": {
"properties": {
"new_title": {
"type": "string",
"title": "New Title",
},
"new_size": {
"type": "integer",
"title": "New Size",
},
"new_description": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "New Description",
},
"new_sub": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem"
},
"new_multi": {
"items": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem"
},
"type": "array",
"title": "New Multi",
"default": [],
},
},
"type": "object",
"required": ["new_title", "new_size", "new_sub"],
"title": "Item",
},
"tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList": {
"properties": {"name2": {"type": "string", "title": "Name2"}},
"type": "object",
"required": ["name2"],
"title": "ItemInList",
},
"tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem": {
"properties": {
"new_sub_name": {
"type": "string",
"title": "New Sub Name",
}
},
"type": "object",
"required": ["new_sub_name"],
"title": "SubItem",
},
"tests__test_pydantic_v1_v2_multifile__modelsv2b__Item": {
"properties": {
"dup_title": {
"type": "string",
"title": "Dup Title",
},
"dup_size": {
"type": "integer",
"title": "Dup Size",
},
"dup_description": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Dup Description",
},
"dup_sub": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem"
},
"dup_multi": {
"items": {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem"
},
"type": "array",
"title": "Dup Multi",
"default": [],
},
},
"type": "object",
"required": ["dup_title", "dup_size", "dup_sub"],
"title": "Item",
},
"tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList": {
"properties": {
"dup_name2": {
"type": "string",
"title": "Dup Name2",
}
},
"type": "object",
"required": ["dup_name2"],
"title": "ItemInList",
},
"tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem": {
"properties": {
"dup_sub_name": {
"type": "string",
"title": "Dup Sub Name",
}
},
"type": "object",
"required": ["dup_sub_name"],
"title": "SubItem",
},
},
},
}
)

View File

@@ -1,692 +0,0 @@
import sys
import warnings
from typing import Any, Union
from tests.utils import skip_module_if_py_gte_314
if sys.version_info >= (3, 14):
skip_module_if_py_gte_314()
from fastapi import FastAPI
from fastapi._compat.v1 import BaseModel
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from pydantic import BaseModel as NewBaseModel
class SubItem(BaseModel):
name: str
class Item(BaseModel):
title: str
size: int
description: Union[str, None] = None
sub: SubItem
multi: list[SubItem] = []
class NewSubItem(NewBaseModel):
new_sub_name: str
class NewItem(NewBaseModel):
new_title: str
new_size: int
new_description: Union[str, None] = None
new_sub: NewSubItem
new_multi: list[NewSubItem] = []
app = FastAPI()
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.post("/v1-to-v2/")
def handle_v1_item_to_v2(data: Item) -> Union[NewItem, None]:
if data.size < 0:
return None
return NewItem(
new_title=data.title,
new_size=data.size,
new_description=data.description,
new_sub=NewSubItem(new_sub_name=data.sub.name),
new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi],
)
@app.post("/v1-to-v2/item-filter", response_model=Union[NewItem, None])
def handle_v1_item_to_v2_filter(data: Item) -> Any:
if data.size < 0:
return None
result = {
"new_title": data.title,
"new_size": data.size,
"new_description": data.description,
"new_sub": {
"new_sub_name": data.sub.name,
"new_sub_secret": "sub_hidden",
},
"new_multi": [
{"new_sub_name": s.name, "new_sub_secret": "sub_hidden"}
for s in data.multi
],
"secret": "hidden_v1_to_v2",
}
return result
@app.post("/v2-to-v1/item")
def handle_v2_item_to_v1(data: NewItem) -> Union[Item, None]:
if data.new_size < 0:
return None
return Item(
title=data.new_title,
size=data.new_size,
description=data.new_description,
sub=SubItem(name=data.new_sub.new_sub_name),
multi=[SubItem(name=s.new_sub_name) for s in data.new_multi],
)
@app.post("/v2-to-v1/item-filter", response_model=Union[Item, None])
def handle_v2_item_to_v1_filter(data: NewItem) -> Any:
if data.new_size < 0:
return None
result = {
"title": data.new_title,
"size": data.new_size,
"description": data.new_description,
"sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"},
"multi": [
{"name": s.new_sub_name, "sub_secret": "sub_hidden"}
for s in data.new_multi
],
"secret": "hidden_v2_to_v1",
}
return result
client = TestClient(app)
def test_v1_to_v2_item_success():
response = client.post(
"/v1-to-v2/",
json={
"title": "Old Item",
"size": 100,
"description": "V1 description",
"sub": {"name": "V1 Sub"},
"multi": [{"name": "M1"}, {"name": "M2"}],
},
)
assert response.status_code == 200, response.text
assert response.json() == {
"new_title": "Old Item",
"new_size": 100,
"new_description": "V1 description",
"new_sub": {"new_sub_name": "V1 Sub"},
"new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}],
}
def test_v1_to_v2_item_returns_none():
response = client.post(
"/v1-to-v2/",
json={"title": "Invalid Item", "size": -10, "sub": {"name": "Sub"}},
)
assert response.status_code == 200, response.text
assert response.json() is None
def test_v1_to_v2_item_minimal():
response = client.post(
"/v1-to-v2/", json={"title": "Minimal", "size": 50, "sub": {"name": "MinSub"}}
)
assert response.status_code == 200, response.text
assert response.json() == {
"new_title": "Minimal",
"new_size": 50,
"new_description": None,
"new_sub": {"new_sub_name": "MinSub"},
"new_multi": [],
}
def test_v1_to_v2_item_filter_success():
response = client.post(
"/v1-to-v2/item-filter",
json={
"title": "Filtered Item",
"size": 50,
"sub": {"name": "Sub"},
"multi": [{"name": "Multi1"}],
},
)
assert response.status_code == 200, response.text
result = response.json()
assert result["new_title"] == "Filtered Item"
assert result["new_size"] == 50
assert result["new_sub"]["new_sub_name"] == "Sub"
assert result["new_multi"][0]["new_sub_name"] == "Multi1"
# Verify secret fields are filtered out
assert "secret" not in result
assert "new_sub_secret" not in result["new_sub"]
assert "new_sub_secret" not in result["new_multi"][0]
def test_v1_to_v2_item_filter_returns_none():
response = client.post(
"/v1-to-v2/item-filter",
json={"title": "Invalid", "size": -1, "sub": {"name": "Sub"}},
)
assert response.status_code == 200, response.text
assert response.json() is None
def test_v2_to_v1_item_success():
response = client.post(
"/v2-to-v1/item",
json={
"new_title": "New Item",
"new_size": 200,
"new_description": "V2 description",
"new_sub": {"new_sub_name": "V2 Sub"},
"new_multi": [{"new_sub_name": "N1"}, {"new_sub_name": "N2"}],
},
)
assert response.status_code == 200, response.text
assert response.json() == {
"title": "New Item",
"size": 200,
"description": "V2 description",
"sub": {"name": "V2 Sub"},
"multi": [{"name": "N1"}, {"name": "N2"}],
}
def test_v2_to_v1_item_returns_none():
response = client.post(
"/v2-to-v1/item",
json={
"new_title": "Invalid New",
"new_size": -5,
"new_sub": {"new_sub_name": "NewSub"},
},
)
assert response.status_code == 200, response.text
assert response.json() is None
def test_v2_to_v1_item_minimal():
response = client.post(
"/v2-to-v1/item",
json={
"new_title": "MinimalNew",
"new_size": 75,
"new_sub": {"new_sub_name": "MinNewSub"},
},
)
assert response.status_code == 200, response.text
assert response.json() == {
"title": "MinimalNew",
"size": 75,
"description": None,
"sub": {"name": "MinNewSub"},
"multi": [],
}
def test_v2_to_v1_item_filter_success():
response = client.post(
"/v2-to-v1/item-filter",
json={
"new_title": "Filtered New",
"new_size": 75,
"new_sub": {"new_sub_name": "NewSub"},
"new_multi": [],
},
)
assert response.status_code == 200, response.text
result = response.json()
assert result["title"] == "Filtered New"
assert result["size"] == 75
assert result["sub"]["name"] == "NewSub"
# Verify secret fields are filtered out
assert "secret" not in result
assert "sub_secret" not in result["sub"]
def test_v2_to_v1_item_filter_returns_none():
response = client.post(
"/v2-to-v1/item-filter",
json={
"new_title": "Invalid Filtered",
"new_size": -100,
"new_sub": {"new_sub_name": "Sub"},
},
)
assert response.status_code == 200, response.text
assert response.json() is None
def test_v1_to_v2_validation_error():
response = client.post("/v1-to-v2/", json={"title": "Missing fields"})
assert response.status_code == 422, response.text
assert response.json() == snapshot(
{
"detail": [
{
"loc": ["body", "size"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "sub"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
def test_v1_to_v2_nested_validation_error():
response = client.post(
"/v1-to-v2/",
json={"title": "Bad sub", "size": 100, "sub": {"wrong_field": "value"}},
)
assert response.status_code == 422, response.text
error_detail = response.json()["detail"]
assert len(error_detail) == 1
assert error_detail[0]["loc"] == ["body", "sub", "name"]
def test_v1_to_v2_type_validation_error():
response = client.post(
"/v1-to-v2/",
json={"title": "Bad type", "size": "not_a_number", "sub": {"name": "Sub"}},
)
assert response.status_code == 422, response.text
error_detail = response.json()["detail"]
assert len(error_detail) == 1
assert error_detail[0]["loc"] == ["body", "size"]
def test_v2_to_v1_validation_error():
response = client.post("/v2-to-v1/item", json={"new_title": "Missing fields"})
assert response.status_code == 422, response.text
assert response.json() == snapshot(
{
"detail": [
{
"type": "missing",
"loc": ["body", "new_size"],
"msg": "Field required",
"input": {"new_title": "Missing fields"},
},
{
"type": "missing",
"loc": ["body", "new_sub"],
"msg": "Field required",
"input": {"new_title": "Missing fields"},
},
]
}
)
def test_v2_to_v1_nested_validation_error():
response = client.post(
"/v2-to-v1/item",
json={
"new_title": "Bad sub",
"new_size": 200,
"new_sub": {"wrong_field": "value"},
},
)
assert response.status_code == 422, response.text
assert response.json() == snapshot(
{
"detail": [
{
"type": "missing",
"loc": ["body", "new_sub", "new_sub_name"],
"msg": "Field required",
"input": {"wrong_field": "value"},
}
]
}
)
def test_v2_to_v1_type_validation_error():
response = client.post(
"/v2-to-v1/item",
json={
"new_title": "Bad type",
"new_size": "not_a_number",
"new_sub": {"new_sub_name": "Sub"},
},
)
assert response.status_code == 422, response.text
assert response.json() == snapshot(
{
"detail": [
{
"type": "int_parsing",
"loc": ["body", "new_size"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "not_a_number",
}
]
}
)
def test_v1_to_v2_with_multi_items():
response = client.post(
"/v1-to-v2/",
json={
"title": "Complex Item",
"size": 300,
"description": "Item with multiple sub-items",
"sub": {"name": "Main Sub"},
"multi": [{"name": "Sub1"}, {"name": "Sub2"}, {"name": "Sub3"}],
},
)
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"new_title": "Complex Item",
"new_size": 300,
"new_description": "Item with multiple sub-items",
"new_sub": {"new_sub_name": "Main Sub"},
"new_multi": [
{"new_sub_name": "Sub1"},
{"new_sub_name": "Sub2"},
{"new_sub_name": "Sub3"},
],
}
)
def test_v2_to_v1_with_multi_items():
response = client.post(
"/v2-to-v1/item",
json={
"new_title": "Complex New Item",
"new_size": 400,
"new_description": "New item with multiple sub-items",
"new_sub": {"new_sub_name": "Main New Sub"},
"new_multi": [{"new_sub_name": "NewSub1"}, {"new_sub_name": "NewSub2"}],
},
)
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"title": "Complex New Item",
"size": 400,
"description": "New item with multiple sub-items",
"sub": {"name": "Main New Sub"},
"multi": [{"name": "NewSub1"}, {"name": "NewSub2"}],
}
)
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/v1-to-v2/": {
"post": {
"summary": "Handle V1 Item To V2",
"operationId": "handle_v1_item_to_v2_v1_to_v2__post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"allOf": [
{"$ref": "#/components/schemas/Item"}
],
"title": "Data",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/NewItem"
},
{"type": "null"},
],
"title": "Response Handle V1 Item To V2 V1 To V2 Post",
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/v1-to-v2/item-filter": {
"post": {
"summary": "Handle V1 Item To V2 Filter",
"operationId": "handle_v1_item_to_v2_filter_v1_to_v2_item_filter_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"allOf": [
{"$ref": "#/components/schemas/Item"}
],
"title": "Data",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/NewItem"
},
{"type": "null"},
],
"title": "Response Handle V1 Item To V2 Filter V1 To V2 Item Filter Post",
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/v2-to-v1/item": {
"post": {
"summary": "Handle V2 Item To V1",
"operationId": "handle_v2_item_to_v1_v2_to_v1_item_post",
"requestBody": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/NewItem"}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/v2-to-v1/item-filter": {
"post": {
"summary": "Handle V2 Item To V1 Filter",
"operationId": "handle_v2_item_to_v1_filter_v2_to_v1_item_filter_post",
"requestBody": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/NewItem"}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
},
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail",
}
},
"type": "object",
"title": "HTTPValidationError",
},
"Item": {
"properties": {
"title": {"type": "string", "title": "Title"},
"size": {"type": "integer", "title": "Size"},
"description": {"type": "string", "title": "Description"},
"sub": {"$ref": "#/components/schemas/SubItem"},
"multi": {
"items": {"$ref": "#/components/schemas/SubItem"},
"type": "array",
"title": "Multi",
"default": [],
},
},
"type": "object",
"required": ["title", "size", "sub"],
"title": "Item",
},
"NewItem": {
"properties": {
"new_title": {"type": "string", "title": "New Title"},
"new_size": {"type": "integer", "title": "New Size"},
"new_description": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "New Description",
},
"new_sub": {"$ref": "#/components/schemas/NewSubItem"},
"new_multi": {
"items": {"$ref": "#/components/schemas/NewSubItem"},
"type": "array",
"title": "New Multi",
"default": [],
},
},
"type": "object",
"required": ["new_title", "new_size", "new_sub"],
"title": "NewItem",
},
"NewSubItem": {
"properties": {
"new_sub_name": {"type": "string", "title": "New Sub Name"}
},
"type": "object",
"required": ["new_sub_name"],
"title": "NewSubItem",
},
"SubItem": {
"properties": {"name": {"type": "string", "title": "Name"}},
"type": "object",
"required": ["name"],
"title": "SubItem",
},
"ValidationError": {
"properties": {
"loc": {
"items": {
"anyOf": [{"type": "string"}, {"type": "integer"}]
},
"type": "array",
"title": "Location",
},
"msg": {"type": "string", "title": "Message"},
"type": {"type": "string", "title": "Error Type"},
},
"type": "object",
"required": ["loc", "msg", "type"],
"title": "ValidationError",
},
}
},
}
)

View File

@@ -1,12 +1,9 @@
import warnings
from typing import Any
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict
from .utils import needs_pydanticv1
def test_read_with_orm_mode() -> None:
class PersonBase(BaseModel):
@@ -44,50 +41,3 @@ def test_read_with_orm_mode() -> None:
assert data["name"] == person_data["name"]
assert data["lastname"] == person_data["lastname"]
assert data["full_name"] == person_data["name"] + " " + person_data["lastname"]
@needs_pydanticv1
def test_read_with_orm_mode_pv1() -> None:
from pydantic import v1
class PersonBase(v1.BaseModel):
name: str
lastname: str
class Person(PersonBase):
@property
def full_name(self) -> str:
return f"{self.name} {self.lastname}"
class Config:
orm_mode = True
read_with_orm_mode = True
class PersonCreate(PersonBase):
pass
class PersonRead(PersonBase):
full_name: str
class Config:
orm_mode = True
app = FastAPI()
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.post("/people/", response_model=PersonRead)
def create_person(person: PersonCreate) -> Any:
db_person = Person.from_orm(person)
return db_person
client = TestClient(app)
person_data = {"name": "Dive", "lastname": "Wilson"}
response = client.post("/people/", json=person_data)
data = response.json()
assert response.status_code == 200, response.text
assert data["name"] == person_data["name"]
assert data["lastname"] == person_data["lastname"]
assert data["full_name"] == person_data["name"] + " " + person_data["lastname"]

View File

@@ -1,4 +1,3 @@
import warnings
from typing import Union
import pytest
@@ -8,8 +7,6 @@ from fastapi.responses import JSONResponse, Response
from fastapi.testclient import TestClient
from pydantic import BaseModel
from tests.utils import needs_pydanticv1
class BaseUser(BaseModel):
name: str
@@ -512,29 +509,6 @@ def test_invalid_response_model_field():
assert "parameter response_model=None" in e.value.args[0]
# TODO: remove when dropping Pydantic v1 support
@needs_pydanticv1
def test_invalid_response_model_field_pv1():
from fastapi._compat import v1
app = FastAPI()
class Model(v1.BaseModel):
foo: str
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
with pytest.raises(FastAPIError) as e:
@app.get("/")
def read_root() -> Union[Response, Model, None]:
return Response(content="Foo") # pragma: no cover
assert "valid Pydantic field type" in e.value.args[0]
assert "parameter response_model=None" in e.value.args[0]
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text

View File

@@ -1,115 +0,0 @@
import importlib
import pytest
from fastapi.testclient import TestClient
from ...utils import needs_pydanticv1
@pytest.fixture(
name="client",
params=[
pytest.param("tutorial007_pv1_py39"),
],
)
def get_client(request: pytest.FixtureRequest):
mod = importlib.import_module(
f"docs_src.path_operation_advanced_configuration.{request.param}"
)
client = TestClient(mod.app)
return client
@needs_pydanticv1
def test_post(client: TestClient):
yaml_data = """
name: Deadpoolio
tags:
- x-force
- x-men
- x-avengers
"""
response = client.post("/items/", content=yaml_data)
assert response.status_code == 200, response.text
assert response.json() == {
"name": "Deadpoolio",
"tags": ["x-force", "x-men", "x-avengers"],
}
@needs_pydanticv1
def test_post_broken_yaml(client: TestClient):
yaml_data = """
name: Deadpoolio
tags:
x - x-force
x - x-men
x - x-avengers
"""
response = client.post("/items/", content=yaml_data)
assert response.status_code == 422, response.text
assert response.json() == {"detail": "Invalid YAML"}
@needs_pydanticv1
def test_post_invalid(client: TestClient):
yaml_data = """
name: Deadpoolio
tags:
- x-force
- x-men
- x-avengers
- sneaky: object
"""
response = client.post("/items/", content=yaml_data)
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{"loc": ["tags", 3], "msg": "str type expected", "type": "type_error.str"}
]
}
@needs_pydanticv1
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/items/": {
"post": {
"summary": "Create Item",
"operationId": "create_item_items__post",
"requestBody": {
"content": {
"application/x-yaml": {
"schema": {
"title": "Item",
"required": ["name", "tags"],
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"tags": {
"title": "Tags",
"type": "array",
"items": {"type": "string"},
},
},
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
}
}
},
}

View File

@@ -1,31 +0,0 @@
import sys
from typing import Any
import pytest
from tests.utils import skip_module_if_py_gte_314
if sys.version_info >= (3, 14):
skip_module_if_py_gte_314()
import importlib
from ...utils import needs_py310
@pytest.fixture(
name="mod",
params=[
"tutorial001_an_py39",
pytest.param("tutorial001_an_py310", marks=needs_py310),
],
)
def get_mod(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}")
return mod
def test_model(mod: Any):
item = mod.Item(name="Foo", size=3.4)
assert item.dict() == {"name": "Foo", "description": None, "size": 3.4}

View File

@@ -1,143 +0,0 @@
import sys
import warnings
import pytest
from fastapi.exceptions import FastAPIDeprecationWarning
from inline_snapshot import snapshot
from tests.utils import skip_module_if_py_gte_314
if sys.version_info >= (3, 14):
skip_module_if_py_gte_314()
import importlib
from fastapi.testclient import TestClient
from ...utils import needs_py310
@pytest.fixture(
name="client",
params=[
"tutorial002_an_py39",
pytest.param("tutorial002_an_py310", marks=needs_py310),
],
)
def get_client(request: pytest.FixtureRequest):
with warnings.catch_warnings(record=True):
warnings.filterwarnings(
"ignore",
message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*",
category=FastAPIDeprecationWarning,
)
mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}")
c = TestClient(mod.app)
return c
def test_call(client: TestClient):
response = client.post("/items/", json={"name": "Foo", "size": 3.4})
assert response.status_code == 200, response.text
assert response.json() == {
"name": "Foo",
"description": None,
"size": 3.4,
}
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/items/": {
"post": {
"summary": "Create Item",
"operationId": "create_item_items__post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"allOf": [
{"$ref": "#/components/schemas/Item"}
],
"title": "Item",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
}
},
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail",
}
},
"type": "object",
"title": "HTTPValidationError",
},
"Item": {
"properties": {
"name": {"type": "string", "title": "Name"},
"description": {"type": "string", "title": "Description"},
"size": {"type": "number", "title": "Size"},
},
"type": "object",
"required": ["name", "size"],
"title": "Item",
},
"ValidationError": {
"properties": {
"loc": {
"items": {
"anyOf": [{"type": "string"}, {"type": "integer"}]
},
"type": "array",
"title": "Location",
},
"msg": {"type": "string", "title": "Message"},
"type": {"type": "string", "title": "Error Type"},
},
"type": "object",
"required": ["loc", "msg", "type"],
"title": "ValidationError",
},
}
},
}
)

View File

@@ -1,158 +0,0 @@
import sys
import warnings
import pytest
from fastapi.exceptions import FastAPIDeprecationWarning
from inline_snapshot import snapshot
from tests.utils import skip_module_if_py_gte_314
if sys.version_info >= (3, 14):
skip_module_if_py_gte_314()
import importlib
from fastapi.testclient import TestClient
from ...utils import needs_py310
@pytest.fixture(
name="client",
params=[
"tutorial003_an_py39",
pytest.param("tutorial003_an_py310", marks=needs_py310),
],
)
def get_client(request: pytest.FixtureRequest):
with warnings.catch_warnings(record=True):
warnings.filterwarnings(
"ignore",
message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*",
category=FastAPIDeprecationWarning,
)
mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}")
c = TestClient(mod.app)
return c
def test_call(client: TestClient):
response = client.post("/items/", json={"name": "Foo", "size": 3.4})
assert response.status_code == 200, response.text
assert response.json() == {
"name": "Foo",
"description": None,
"size": 3.4,
}
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/items/": {
"post": {
"summary": "Create Item",
"operationId": "create_item_items__post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"allOf": [
{"$ref": "#/components/schemas/Item"}
],
"title": "Item",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ItemV2"
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
}
},
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail",
}
},
"type": "object",
"title": "HTTPValidationError",
},
"Item": {
"properties": {
"name": {"type": "string", "title": "Name"},
"description": {"type": "string", "title": "Description"},
"size": {"type": "number", "title": "Size"},
},
"type": "object",
"required": ["name", "size"],
"title": "Item",
},
"ItemV2": {
"properties": {
"name": {"type": "string", "title": "Name"},
"description": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Description",
},
"size": {"type": "number", "title": "Size"},
},
"type": "object",
"required": ["name", "size"],
"title": "ItemV2",
},
"ValidationError": {
"properties": {
"loc": {
"items": {
"anyOf": [{"type": "string"}, {"type": "integer"}]
},
"type": "array",
"title": "Location",
},
"msg": {"type": "string", "title": "Message"},
"type": {"type": "string", "title": "Error Type"},
},
"type": "object",
"required": ["loc", "msg", "type"],
"title": "ValidationError",
},
}
},
}
)

View File

@@ -1,156 +0,0 @@
import sys
import warnings
import pytest
from fastapi.exceptions import FastAPIDeprecationWarning
from inline_snapshot import snapshot
from tests.utils import skip_module_if_py_gte_314
if sys.version_info >= (3, 14):
skip_module_if_py_gte_314()
import importlib
from fastapi.testclient import TestClient
from ...utils import needs_py310
@pytest.fixture(
name="client",
params=[
pytest.param("tutorial004_an_py39"),
pytest.param("tutorial004_an_py310", marks=needs_py310),
],
)
def get_client(request: pytest.FixtureRequest):
with warnings.catch_warnings(record=True):
warnings.filterwarnings(
"ignore",
message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*",
category=FastAPIDeprecationWarning,
)
mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}")
c = TestClient(mod.app)
return c
def test_call(client: TestClient):
response = client.post("/items/", json={"item": {"name": "Foo", "size": 3.4}})
assert response.status_code == 200, response.text
assert response.json() == {
"name": "Foo",
"description": None,
"size": 3.4,
}
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/items/": {
"post": {
"summary": "Create Item",
"operationId": "create_item_items__post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"allOf": [
{
"$ref": "#/components/schemas/Body_create_item_items__post"
}
],
"title": "Body",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
}
},
"components": {
"schemas": {
"Body_create_item_items__post": {
"properties": {
"item": {
"allOf": [{"$ref": "#/components/schemas/Item"}],
"title": "Item",
}
},
"type": "object",
"required": ["item"],
"title": "Body_create_item_items__post",
},
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail",
}
},
"type": "object",
"title": "HTTPValidationError",
},
"Item": {
"properties": {
"name": {"type": "string", "title": "Name"},
"description": {"type": "string", "title": "Description"},
"size": {"type": "number", "title": "Size"},
},
"type": "object",
"required": ["name", "size"],
"title": "Item",
},
"ValidationError": {
"properties": {
"loc": {
"items": {
"anyOf": [{"type": "string"}, {"type": "integer"}]
},
"type": "array",
"title": "Location",
},
"msg": {"type": "string", "title": "Message"},
"type": {"type": "string", "title": "Error Type"},
},
"type": "object",
"required": ["loc", "msg", "type"],
"title": "ValidationError",
},
}
},
}
)

View File

@@ -1,128 +0,0 @@
import importlib
import warnings
import pytest
from fastapi.exceptions import FastAPIDeprecationWarning
from fastapi.testclient import TestClient
from ...utils import needs_pydanticv1
@pytest.fixture(
name="client",
params=[
"tutorial002_pv1_py39",
"tutorial002_pv1_an_py39",
],
)
def get_client(request: pytest.FixtureRequest):
with warnings.catch_warnings(record=True):
warnings.filterwarnings(
"ignore",
message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*",
category=FastAPIDeprecationWarning,
)
mod = importlib.import_module(f"docs_src.request_form_models.{request.param}")
client = TestClient(mod.app)
return client
# TODO: remove when deprecating Pydantic v1
@needs_pydanticv1
def test_post_body_form(client: TestClient):
response = client.post("/login/", data={"username": "Foo", "password": "secret"})
assert response.status_code == 200
assert response.json() == {"username": "Foo", "password": "secret"}
# TODO: remove when deprecating Pydantic v1
@needs_pydanticv1
def test_post_body_extra_form(client: TestClient):
response = client.post(
"/login/", data={"username": "Foo", "password": "secret", "extra": "extra"}
)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "value_error.extra",
"loc": ["body", "extra"],
"msg": "extra fields not permitted",
}
]
}
# TODO: remove when deprecating Pydantic v1
@needs_pydanticv1
def test_post_body_form_no_password(client: TestClient):
response = client.post("/login/", data={"username": "Foo"})
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "value_error.missing",
"loc": ["body", "password"],
"msg": "field required",
}
]
}
# TODO: remove when deprecating Pydantic v1
@needs_pydanticv1
def test_post_body_form_no_username(client: TestClient):
response = client.post("/login/", data={"password": "secret"})
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "value_error.missing",
"loc": ["body", "username"],
"msg": "field required",
}
]
}
# TODO: remove when deprecating Pydantic v1
@needs_pydanticv1
def test_post_body_form_no_data(client: TestClient):
response = client.post("/login/")
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "value_error.missing",
"loc": ["body", "username"],
"msg": "field required",
},
{
"type": "value_error.missing",
"loc": ["body", "password"],
"msg": "field required",
},
]
}
# TODO: remove when deprecating Pydantic v1
@needs_pydanticv1
def test_post_body_json(client: TestClient):
response = client.post("/login/", json={"username": "Foo", "password": "secret"})
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"type": "value_error.missing",
"loc": ["body", "username"],
"msg": "field required",
},
{
"type": "value_error.missing",
"loc": ["body", "password"],
"msg": "field required",
},
]
}

View File

@@ -1,152 +0,0 @@
import importlib
import warnings
import pytest
from fastapi.exceptions import FastAPIDeprecationWarning
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from ...utils import needs_py310, needs_pydanticv1
@pytest.fixture(
name="client",
params=[
pytest.param("tutorial001_pv1_py39"),
pytest.param("tutorial001_pv1_py310", marks=needs_py310),
],
)
def get_client(request: pytest.FixtureRequest):
with warnings.catch_warnings(record=True):
warnings.filterwarnings(
"ignore",
message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*",
category=FastAPIDeprecationWarning,
)
mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}")
client = TestClient(mod.app)
return client
@needs_pydanticv1
def test_post_body_example(client: TestClient):
response = client.put(
"/items/5",
json={
"name": "Foo",
"description": "A very nice Item",
"price": 35.4,
"tax": 3.2,
},
)
assert response.status_code == 200
@needs_pydanticv1
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/items/{item_id}": {
"put": {
"summary": "Update Item",
"operationId": "update_item_items__item_id__put",
"parameters": [
{
"required": True,
"schema": {"type": "integer", "title": "Item Id"},
"name": "item_id",
"in": "path",
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"title": "Item",
"allOf": [
{"$ref": "#/components/schemas/Item"}
],
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
}
},
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail",
}
},
"type": "object",
"title": "HTTPValidationError",
},
"Item": {
"properties": {
"name": {"type": "string", "title": "Name"},
"description": {"type": "string", "title": "Description"},
"price": {"type": "number", "title": "Price"},
"tax": {"type": "number", "title": "Tax"},
},
"type": "object",
"required": ["name", "price"],
"title": "Item",
"examples": [
{
"name": "Foo",
"description": "A very nice Item",
"price": 35.4,
"tax": 3.2,
}
],
},
"ValidationError": {
"properties": {
"loc": {
"items": {
"anyOf": [{"type": "string"}, {"type": "integer"}]
},
"type": "array",
"title": "Location",
},
"msg": {"type": "string", "title": "Message"},
"type": {"type": "string", "title": "Error Type"},
},
"type": "object",
"required": ["loc", "msg", "type"],
"title": "ValidationError",
},
}
},
}
)