committed by
GitHub
55 changed files with 337 additions and 8784 deletions
@ -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 |
|
||||
@ -1,264 +0,0 @@ |
|||||
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 PydanticSchemaGenerationError as PydanticSchemaGenerationError |
|
||||
from .v2 import RequiredParam as RequiredParam |
|
||||
from .v2 import Undefined as Undefined |
|
||||
from .v2 import UndefinedType as UndefinedType |
|
||||
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 ( |
|
||||
with_info_plain_validator_function as with_info_plain_validator_function, |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@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] |
|
||||
|
|
||||
|
|
||||
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] |
|
||||
|
|
||||
|
|
||||
def is_scalar_sequence_field(field: ModelField) -> bool: |
|
||||
return v2.is_scalar_sequence_field(field) # type: ignore[arg-type] |
|
||||
|
|
||||
|
|
||||
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() |
|
||||
|
|
||||
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()) |
|
||||
all_flat_models = all_flat_models.union(v2_flat_models) # type: ignore[arg-type] |
|
||||
|
|
||||
model_name_map = v2.get_model_name_map(all_flat_models) # type: ignore[arg-type] |
|
||||
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, |
|
||||
], |
|
||||
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, |
|
||||
field_mapping=field_mapping, # type: ignore[arg-type] |
|
||||
separate_input_output_schemas=separate_input_output_schemas, |
|
||||
) |
|
||||
|
|
||||
|
|
||||
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 +0,0 @@ |
|||||
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 |
|
||||
|
|
||||
|
|
||||
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) |
|
||||
] |
|
||||
|
|
||||
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,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()) |
|
||||
@ -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, |
|
||||
) |
|
||||
File diff suppressed because it is too large
@ -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,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} |
|
||||
@ -0,0 +1,97 @@ |
|||||
|
import sys |
||||
|
import warnings |
||||
|
from typing import Union |
||||
|
|
||||
|
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} |
||||
|
|
||||
|
|
||||
|
def test_raises_pydantic_v1_model_in_union() -> None: |
||||
|
class ModelV1A(BaseModel): |
||||
|
name: str |
||||
|
|
||||
|
app = FastAPI() |
||||
|
|
||||
|
with pytest.raises(PydanticV1NotSupportedError): |
||||
|
|
||||
|
@app.post("/union") |
||||
|
def endpoint(data: Union[dict, ModelV1A]): # pragma: no cover |
||||
|
return data |
||||
|
|
||||
|
|
||||
|
def test_raises_pydantic_v1_model_in_sequence() -> None: |
||||
|
class ModelV1A(BaseModel): |
||||
|
name: str |
||||
|
|
||||
|
app = FastAPI() |
||||
|
|
||||
|
with pytest.raises(PydanticV1NotSupportedError): |
||||
|
|
||||
|
@app.post("/sequence") |
||||
|
def endpoint(data: list[ModelV1A]): # pragma: no cover |
||||
|
return data |
||||
@ -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
@ -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,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", |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
||||
) |
|
||||
@ -5,8 +5,6 @@ import pytest |
|||||
from fastapi.testclient import TestClient |
from fastapi.testclient import TestClient |
||||
from pytest import MonkeyPatch |
from pytest import MonkeyPatch |
||||
|
|
||||
from ...utils import needs_pydanticv1 |
|
||||
|
|
||||
|
|
||||
@pytest.fixture( |
@pytest.fixture( |
||||
name="mod_path", |
name="mod_path", |
||||
@ -34,16 +32,6 @@ def test_settings(main_mod: ModuleType, monkeypatch: MonkeyPatch): |
|||||
assert settings.items_per_user == 50 |
assert settings.items_per_user == 50 |
||||
|
|
||||
|
|
||||
@needs_pydanticv1 |
|
||||
def test_settings_pv1(mod_path: str, monkeypatch: MonkeyPatch): |
|
||||
monkeypatch.setenv("ADMIN_EMAIL", "[email protected]") |
|
||||
config_mod = importlib.import_module(f"{mod_path}.config_pv1") |
|
||||
settings = config_mod.Settings() |
|
||||
assert settings.app_name == "Awesome API" |
|
||||
assert settings.admin_email == "[email protected]" |
|
||||
assert settings.items_per_user == 50 |
|
||||
|
|
||||
|
|
||||
def test_endpoint(main_mod: ModuleType, monkeypatch: MonkeyPatch): |
def test_endpoint(main_mod: ModuleType, monkeypatch: MonkeyPatch): |
||||
monkeypatch.setenv("ADMIN_EMAIL", "[email protected]") |
monkeypatch.setenv("ADMIN_EMAIL", "[email protected]") |
||||
client = TestClient(main_mod.app) |
client = TestClient(main_mod.app) |
||||
|
|||||
@ -4,16 +4,8 @@ import pytest |
|||||
from fastapi.testclient import TestClient |
from fastapi.testclient import TestClient |
||||
from pytest import MonkeyPatch |
from pytest import MonkeyPatch |
||||
|
|
||||
from ...utils import needs_pydanticv1 |
|
||||
|
|
||||
|
@pytest.fixture(name="app", params=[pytest.param("tutorial001_py39")]) |
||||
@pytest.fixture( |
|
||||
name="app", |
|
||||
params=[ |
|
||||
pytest.param("tutorial001_py39"), |
|
||||
pytest.param("tutorial001_pv1_py39", marks=needs_pydanticv1), |
|
||||
], |
|
||||
) |
|
||||
def get_app(request: pytest.FixtureRequest, monkeypatch: MonkeyPatch): |
def get_app(request: pytest.FixtureRequest, monkeypatch: MonkeyPatch): |
||||
monkeypatch.setenv("ADMIN_EMAIL", "[email protected]") |
monkeypatch.setenv("ADMIN_EMAIL", "[email protected]") |
||||
mod = importlib.import_module(f"docs_src.settings.{request.param}") |
mod = importlib.import_module(f"docs_src.settings.{request.param}") |
||||
|
|||||
Loading…
Reference in new issue