mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-27 00:01:03 -05:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b32e65f5af | ||
|
|
fc50d9d438 | ||
|
|
4ef0128708 | ||
|
|
814d653192 | ||
|
|
ea33214473 | ||
|
|
e5fa006f80 | ||
|
|
ec213fa26e | ||
|
|
01289a46fc | ||
|
|
0d6652bd32 | ||
|
|
de015ae49d | ||
|
|
2e53a98830 | ||
|
|
92ec1a08a0 | ||
|
|
aa11eb51a2 | ||
|
|
3c8db03107 | ||
|
|
5eacf7ee4c | ||
|
|
7f76702908 | ||
|
|
bde3f1ba9f | ||
|
|
fd58f90369 | ||
|
|
4b0fca4cbd |
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: ...
|
||||
@@ -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):
|
||||
|
||||
@@ -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())
|
||||
@@ -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(".", "__")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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]]:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
109
fastapi/utils.py
109
fastapi/utils.py
@@ -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(
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"}
|
||||
|
||||
@@ -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"},
|
||||
}
|
||||
@@ -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"},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
70
tests/test_pydantic_v1_error.py
Normal file
70
tests/test_pydantic_v1_error.py
Normal 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}
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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),
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {}}},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -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}
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -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",
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user