+++ /dev/null
-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
+++ /dev/null
-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
-from .main import BaseConfig as BaseConfig
-from .main import PydanticSchemaGenerationError as PydanticSchemaGenerationError
-from .main import RequiredParam as RequiredParam
-from .main import Undefined as Undefined
-from .main import UndefinedType as UndefinedType
-from .main import Url as Url
-from .main import Validator as Validator
-from .main import _get_model_config as _get_model_config
-from .main import _is_error_wrapper as _is_error_wrapper
-from .main import _is_model_class as _is_model_class
-from .main import _is_model_field as _is_model_field
-from .main import _is_undefined as _is_undefined
-from .main import _model_dump as _model_dump
-from .main import copy_field_info as copy_field_info
-from .main import create_body_model as create_body_model
-from .main import evaluate_forwardref as evaluate_forwardref
-from .main import get_annotation_from_field_info as get_annotation_from_field_info
-from .main import get_cached_model_fields as get_cached_model_fields
-from .main import get_compat_model_name_map as get_compat_model_name_map
-from .main import get_definitions as get_definitions
-from .main import get_missing_field_error as get_missing_field_error
-from .main import get_schema_from_model_field as get_schema_from_model_field
-from .main import is_bytes_field as is_bytes_field
-from .main import is_bytes_sequence_field as is_bytes_sequence_field
-from .main import is_scalar_field as is_scalar_field
-from .main import is_scalar_sequence_field as is_scalar_sequence_field
-from .main import is_sequence_field as is_sequence_field
-from .main import serialize_sequence_value as serialize_sequence_value
-from .main import (
- with_info_plain_validator_function as with_info_plain_validator_function,
-)
-from .may_v1 import CoreSchema as CoreSchema
-from .may_v1 import GetJsonSchemaHandler as GetJsonSchemaHandler
-from .may_v1 import JsonSchemaValue as JsonSchemaValue
-from .may_v1 import _normalize_errors as _normalize_errors
-from .model_field import ModelField as ModelField
from .shared import PYDANTIC_V2 as PYDANTIC_V2
from .shared import PYDANTIC_VERSION_MINOR_TUPLE as PYDANTIC_VERSION_MINOR_TUPLE
from .shared import annotation_is_pydantic_v1 as annotation_is_pydantic_v1
from .shared import field_annotation_is_scalar as field_annotation_is_scalar
+from .shared import is_pydantic_v1_model_class as is_pydantic_v1_model_class
+from .shared import is_pydantic_v1_model_instance as is_pydantic_v1_model_instance
from .shared import (
is_uploadfile_or_nonable_uploadfile_annotation as is_uploadfile_or_nonable_uploadfile_annotation,
)
from .shared import lenient_issubclass as lenient_issubclass
from .shared import sequence_types as sequence_types
from .shared import value_is_sequence as value_is_sequence
+from .v2 import BaseConfig as BaseConfig
+from .v2 import ModelField as ModelField
+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 _regenerate_error_with_loc as _regenerate_error_with_loc
+from .v2 import copy_field_info as copy_field_info
+from .v2 import create_body_model as create_body_model
+from .v2 import evaluate_forwardref as evaluate_forwardref
+from .v2 import get_cached_model_fields as get_cached_model_fields
+from .v2 import get_compat_model_name_map as get_compat_model_name_map
+from .v2 import get_definitions as get_definitions
+from .v2 import get_missing_field_error as get_missing_field_error
+from .v2 import get_schema_from_model_field as get_schema_from_model_field
+from .v2 import is_bytes_field as is_bytes_field
+from .v2 import is_bytes_sequence_field as is_bytes_sequence_field
+from .v2 import is_scalar_field as is_scalar_field
+from .v2 import is_scalar_sequence_field as is_scalar_sequence_field
+from .v2 import is_sequence_field as is_sequence_field
+from .v2 import serialize_sequence_value as serialize_sequence_value
+from .v2 import (
+ with_info_plain_validator_function as with_info_plain_validator_function,
+)
+++ /dev/null
-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]
+++ /dev/null
-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
+++ /dev/null
-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: ...
import sys
import types
import typing
+import warnings
from collections import deque
from collections.abc import Mapping, Sequence
from dataclasses import is_dataclass
Union,
)
-from fastapi._compat import may_v1
from fastapi.types import UnionType
from pydantic import BaseModel
from pydantic.version import VERSION as PYDANTIC_VERSION
def _annotation_is_complex(annotation: Union[type[Any], None]) -> bool:
return (
- lenient_issubclass(
- annotation, (BaseModel, may_v1.BaseModel, Mapping, UploadFile)
- )
+ lenient_issubclass(annotation, (BaseModel, Mapping, UploadFile))
or _annotation_is_sequence(annotation)
or is_dataclass(annotation)
)
)
+def is_pydantic_v1_model_instance(obj: Any) -> bool:
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", UserWarning)
+ from pydantic import v1
+ return isinstance(obj, v1.BaseModel)
+
+
+def is_pydantic_v1_model_class(cls: Any) -> bool:
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", UserWarning)
+ from pydantic import v1
+ return lenient_issubclass(cls, v1.BaseModel)
+
+
def annotation_is_pydantic_v1(annotation: Any) -> bool:
- if lenient_issubclass(annotation, may_v1.BaseModel):
+ if is_pydantic_v1_model_class(annotation):
return True
origin = get_origin(annotation)
if origin is Union or origin is UnionType:
for arg in get_args(annotation):
- if lenient_issubclass(arg, may_v1.BaseModel):
+ if is_pydantic_v1_model_class(arg):
return True
if field_annotation_is_sequence(annotation):
for sub_annotation in get_args(annotation):
+++ /dev/null
-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())
from copy import copy, deepcopy
from dataclasses import dataclass, is_dataclass
from enum import Enum
+from functools import lru_cache
from typing import (
Annotated,
Any,
cast,
)
-from fastapi._compat import may_v1, shared
+from fastapi._compat import shared
from fastapi.openapi.constants import REF_TEMPLATE
from fastapi.types import IncEx, ModelNameMap, UnionType
from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, create_model
None,
)
except ValidationError as exc:
- return None, may_v1._regenerate_error_with_loc(
+ return None, _regenerate_error_with_loc(
errors=exc.errors(include_url=False), loc_prefix=loc
)
return id(self)
-def get_annotation_from_field_info(
- annotation: Any, field_info: FieldInfo, field_name: str
-) -> Any:
- return annotation
-
-
-def _model_dump(
- model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
-) -> Any:
- return model.model_dump(mode=mode, **kwargs)
-
-
-def _get_model_config(model: BaseModel) -> Any:
- return model.model_config
-
-
def _has_computed_fields(field: ModelField) -> bool:
computed_fields = field._type_adapter.core_schema.get("schema", {}).get(
"computed_fields", []
return model_fields
+@lru_cache
+def get_cached_model_fields(model: type[BaseModel]) -> list[ModelField]:
+ return get_model_fields(model) # type: ignore[return-value]
+
+
# Duplicate of several schema functions from Pydantic v1 to make them compatible with
# Pydantic v2 and allow mixing the models
def get_model_name_map(unique_models: TypeModelSet) -> dict[TypeModelOrEnum, str]:
name_model_map = {}
- conflicting_names: set[str] = set()
for model in unique_models:
model_name = normalize_name(model.__name__)
- if model_name in conflicting_names:
- model_name = get_long_model_name(model)
- name_model_map[model_name] = model
- elif model_name in name_model_map:
- conflicting_names.add(model_name)
- conflicting_model = name_model_map.pop(model_name)
- name_model_map[get_long_model_name(conflicting_model)] = conflicting_model
- name_model_map[get_long_model_name(model)] = model
- else:
- name_model_map[model_name] = model
+ name_model_map[model_name] = model
return {v: k for k, v in name_model_map.items()}
+def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap:
+ all_flat_models = set()
+
+ v2_model_fields = [field for field in fields if isinstance(field, ModelField)]
+ v2_flat_models = 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 = get_model_name_map(all_flat_models) # type: ignore[arg-type]
+ return model_name_map
+
+
def get_flat_models_from_model(
model: type["BaseModel"], known_models: Union[TypeModelSet, None] = None
) -> TypeModelSet:
return known_models
-def get_long_model_name(model: TypeModelOrEnum) -> str:
- return f"{model.__module__}__{model.__qualname__}".replace(".", "__")
+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 errors
+ ]
+
+ return updated_loc_errors
+from collections.abc import Mapping
from typing import (
Annotated,
Any,
)
from annotated_doc import Doc
-from fastapi._compat import (
- CoreSchema,
- GetJsonSchemaHandler,
- JsonSchemaValue,
-)
+from pydantic import GetJsonSchemaHandler
from starlette.datastructures import URL as URL # noqa: F401
from starlette.datastructures import Address as Address # noqa: F401
from starlette.datastructures import FormData as FormData # noqa: F401
@classmethod
def __get_pydantic_json_schema__(
- cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
- ) -> JsonSchemaValue:
+ cls, core_schema: Mapping[str, Any], handler: GetJsonSchemaHandler
+ ) -> dict[str, Any]:
return {"type": "string", "format": "binary"}
@classmethod
def __get_pydantic_core_schema__(
- cls, source: type[Any], handler: Callable[[Any], CoreSchema]
- ) -> CoreSchema:
+ cls, source: type[Any], handler: Callable[[Any], Mapping[str, Any]]
+ ) -> Mapping[str, Any]:
from ._compat.v2 import with_info_plain_validator_function
return with_info_plain_validator_function(cls._validate)
import dataclasses
import inspect
import sys
-import warnings
from collections.abc import Coroutine, Mapping, Sequence
from contextlib import AsyncExitStack, contextmanager
from copy import copy, deepcopy
ModelField,
RequiredParam,
Undefined,
- _is_error_wrapper,
- _is_model_class,
+ _regenerate_error_with_loc,
copy_field_info,
create_body_model,
evaluate_forwardref,
field_annotation_is_scalar,
- get_annotation_from_field_info,
get_cached_model_fields,
get_missing_field_error,
is_bytes_field,
is_uploadfile_or_nonable_uploadfile_annotation,
is_uploadfile_sequence_annotation,
lenient_issubclass,
- may_v1,
sequence_types,
serialize_sequence_value,
value_is_sequence,
)
-from fastapi._compat.shared import annotation_is_pydantic_v1
from fastapi.background import BackgroundTasks
from fastapi.concurrency import (
asynccontextmanager,
contextmanager_in_threadpool,
)
from fastapi.dependencies.models import Dependant
-from fastapi.exceptions import DependencyScopeError, FastAPIDeprecationWarning
+from fastapi.exceptions import DependencyScopeError
from fastapi.logger import logger
from fastapi.security.oauth2 import SecurityScopes
from fastapi.types import DependencyCacheKey
from starlette.websockets import WebSocket
from typing_extensions import Literal, get_args, get_origin
-from .. import temp_pydantic_v1_params
-
multipart_not_installed_error = (
'Form data requires "python-multipart" to be installed. \n'
'You can install "python-multipart" with: \n\n'
if not fields:
return fields
first_field = fields[0]
- if len(fields) == 1 and _is_model_class(first_field.type_):
+ if len(fields) == 1 and lenient_issubclass(first_field.type_, BaseModel):
fields_to_extract = get_cached_model_fields(first_field.type_)
return fields_to_extract
return fields
)
continue
assert param_details.field is not None
- if isinstance(param_details.field, may_v1.ModelField):
- warnings.warn(
- "pydantic.v1 is deprecated and will soon stop being supported by FastAPI."
- f" Please update the param {param_name}: {param_details.type_annotation!r}.",
- category=FastAPIDeprecationWarning,
- stacklevel=5,
- )
- if isinstance(
- param_details.field.field_info, (params.Body, temp_pydantic_v1_params.Body)
- ):
+ if isinstance(param_details.field.field_info, params.Body):
dependant.body_params.append(param_details.field)
else:
add_param_to_fields(field=param_details.field, dependant=dependant)
fastapi_annotations = [
arg
for arg in annotated_args[1:]
- if isinstance(arg, (FieldInfo, may_v1.FieldInfo, params.Depends))
+ if isinstance(arg, (FieldInfo, params.Depends))
]
fastapi_specific_annotations = [
arg
arg,
(
params.Param,
- temp_pydantic_v1_params.Param,
params.Body,
- temp_pydantic_v1_params.Body,
params.Depends,
),
)
]
if fastapi_specific_annotations:
- fastapi_annotation: Union[
- FieldInfo, may_v1.FieldInfo, params.Depends, None
- ] = fastapi_specific_annotations[-1]
+ fastapi_annotation: Union[FieldInfo, params.Depends, None] = (
+ fastapi_specific_annotations[-1]
+ )
else:
fastapi_annotation = None
# Set default for Annotated FieldInfo
- if isinstance(fastapi_annotation, (FieldInfo, may_v1.FieldInfo)):
+ if isinstance(fastapi_annotation, FieldInfo):
# Copy `field_info` because we mutate `field_info.default` below.
field_info = copy_field_info(
field_info=fastapi_annotation, # type: ignore[arg-type]
annotation=use_annotation,
)
- assert field_info.default in {
- Undefined,
- may_v1.Undefined,
- } or field_info.default in {RequiredParam, may_v1.RequiredParam}, (
+ assert (
+ field_info.default == Undefined or field_info.default == RequiredParam
+ ), (
f"`{field_info.__class__.__name__}` default value cannot be set in"
f" `Annotated` for {param_name!r}. Set the default value with `=` instead."
)
)
depends = value
# Get FieldInfo from default value
- elif isinstance(value, (FieldInfo, may_v1.FieldInfo)):
+ elif isinstance(value, FieldInfo):
assert field_info is None, (
"Cannot specify FastAPI annotations in `Annotated` and default value"
f" together for {param_name!r}"
) or is_uploadfile_sequence_annotation(type_annotation):
field_info = params.File(annotation=use_annotation, default=default_value)
elif not field_annotation_is_scalar(annotation=type_annotation):
- if annotation_is_pydantic_v1(use_annotation):
- field_info = temp_pydantic_v1_params.Body( # type: ignore[assignment]
- annotation=use_annotation, default=default_value
- )
- else:
- field_info = params.Body(
- annotation=use_annotation, default=default_value
- )
+ field_info = params.Body(annotation=use_annotation, default=default_value)
else:
field_info = params.Query(annotation=use_annotation, default=default_value)
if field_info is not None:
# Handle field_info.in_
if is_path_param:
- assert isinstance(
- field_info, (params.Path, temp_pydantic_v1_params.Path)
- ), (
+ assert isinstance(field_info, params.Path), (
f"Cannot use `{field_info.__class__.__name__}` for path param"
f" {param_name!r}"
)
elif (
- isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param))
+ isinstance(field_info, params.Param)
and getattr(field_info, "in_", None) is None
):
field_info.in_ = params.ParamTypes.query
- use_annotation_from_field_info = get_annotation_from_field_info(
- use_annotation,
- field_info,
- param_name,
- )
- if isinstance(field_info, (params.Form, temp_pydantic_v1_params.Form)):
+ use_annotation_from_field_info = use_annotation
+ if isinstance(field_info, params.Form):
ensure_multipart_is_installed()
if not field_info.alias and getattr(field_info, "convert_underscores", None):
alias = param_name.replace("_", "-")
type_=use_annotation_from_field_info,
default=field_info.default,
alias=alias,
- required=field_info.default
- in (RequiredParam, may_v1.RequiredParam, Undefined),
+ required=field_info.default in (RequiredParam, Undefined),
field_info=field_info,
)
if is_path_param:
assert is_scalar_field(field=field), (
"Path params must be of one of the supported types"
)
- elif isinstance(field_info, (params.Query, temp_pydantic_v1_params.Query)):
+ elif isinstance(field_info, params.Query):
assert (
is_scalar_field(field)
or is_scalar_sequence_field(field)
or (
- _is_model_class(field.type_)
+ lenient_issubclass(field.type_, BaseModel)
# For Pydantic v1
and getattr(field, "shape", 1) == 1
)
else:
return deepcopy(field.default), []
v_, errors_ = field.validate(value, values, loc=loc)
- if _is_error_wrapper(errors_): # type: ignore[arg-type]
- return None, [errors_]
- elif isinstance(errors_, list):
- new_errors = may_v1._regenerate_error_with_loc(errors=errors_, loc_prefix=())
+ if isinstance(errors_, list):
+ new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
return None, new_errors
else:
return v_, []
if (
value is None
or (
- isinstance(field.field_info, (params.Form, temp_pydantic_v1_params.Form))
+ isinstance(field.field_info, params.Form)
and isinstance(value, str) # For type checks
and value == ""
)
if single_not_embedded_field:
field_info = first_field.field_info
- assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), (
+ assert isinstance(field_info, params.Param), (
"Params must be subclasses of Param"
)
loc: tuple[str, ...] = (field_info.in_.value,)
for field in fields:
value = _get_multidict_value(field, received_params)
field_info = field.field_info
- assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), (
+ assert isinstance(field_info, params.Param), (
"Params must be subclasses of Param"
)
loc = (field_info.in_.value, get_validation_alias(field))
union_args = get_args(field_type)
for arg in union_args:
- if not _is_model_class(arg):
+ if not lenient_issubclass(arg, BaseModel):
return False
return True
# If it's a Form (or File) field, it has to be a BaseModel (or a union of BaseModels) to be top level
# otherwise it has to be embedded, so that the key value pair can be extracted
if (
- isinstance(first_field.field_info, (params.Form, temp_pydantic_v1_params.Form))
- and not _is_model_class(first_field.type_)
+ isinstance(first_field.field_info, params.Form)
+ and not lenient_issubclass(first_field.type_, BaseModel)
and not is_union_of_base_models(first_field.type_)
):
return True
value = _get_multidict_value(field, received_body)
field_info = field.field_info
if (
- isinstance(field_info, (params.File, temp_pydantic_v1_params.File))
+ isinstance(field_info, params.File)
and is_bytes_field(field)
and isinstance(value, UploadFile)
):
value = await value.read()
elif (
is_bytes_sequence_field(field)
- and isinstance(field_info, (params.File, temp_pydantic_v1_params.File))
+ and isinstance(field_info, params.File)
and value_is_sequence(value)
):
# For types
if (
single_not_embedded_field
- and _is_model_class(first_field.type_)
+ and lenient_issubclass(first_field.type_, BaseModel)
and isinstance(received_body, FormData)
):
fields_to_extract = get_cached_model_fields(first_field.type_)
BodyFieldInfo_kwargs["default"] = None
if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params):
BodyFieldInfo: type[params.Body] = params.File
- elif any(
- isinstance(f.field_info, temp_pydantic_v1_params.File)
- for f in flat_dependant.body_params
- ):
- BodyFieldInfo: type[temp_pydantic_v1_params.Body] = temp_pydantic_v1_params.File # type: ignore[no-redef]
elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params):
BodyFieldInfo = params.Form
- elif any(
- isinstance(f.field_info, temp_pydantic_v1_params.Form)
- for f in flat_dependant.body_params
- ):
- BodyFieldInfo = temp_pydantic_v1_params.Form # type: ignore[assignment]
else:
- if annotation_is_pydantic_v1(BodyModel):
- BodyFieldInfo = temp_pydantic_v1_params.Body # type: ignore[assignment]
- else:
- BodyFieldInfo = params.Body
+ BodyFieldInfo = params.Body
body_param_media_types = [
f.field_info.media_type
for f in flat_dependant.body_params
- if isinstance(f.field_info, (params.Body, temp_pydantic_v1_params.Body))
+ if isinstance(f.field_info, params.Body)
]
if len(set(body_param_media_types)) == 1:
BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0]
from uuid import UUID
from annotated_doc import Doc
-from fastapi._compat import may_v1
+from fastapi.exceptions import PydanticV1NotSupportedError
from fastapi.types import IncEx
from pydantic import BaseModel
from pydantic.color import Color
from pydantic.networks import AnyUrl, NameEmail
from pydantic.types import SecretBytes, SecretStr
+from pydantic_core import PydanticUndefinedType
-from ._compat import Url, _is_undefined, _model_dump
+from ._compat import (
+ Url,
+ is_pydantic_v1_model_instance,
+)
# Taken from Pydantic v1 as is
ENCODERS_BY_TYPE: dict[type[Any], Callable[[Any], Any]] = {
bytes: lambda o: o.decode(),
Color: str,
- may_v1.Color: str,
datetime.date: isoformat,
datetime.datetime: isoformat,
datetime.time: isoformat,
IPv6Interface: str,
IPv6Network: str,
NameEmail: str,
- may_v1.NameEmail: str,
Path: str,
Pattern: lambda o: o.pattern,
SecretBytes: str,
- may_v1.SecretBytes: str,
SecretStr: str,
- may_v1.SecretStr: str,
set: list,
UUID: str,
Url: str,
- may_v1.Url: str,
AnyUrl: str,
- may_v1.AnyUrl: str,
}
include = set(include)
if exclude is not None and not isinstance(exclude, (set, dict)):
exclude = set(exclude)
- if isinstance(obj, (BaseModel, may_v1.BaseModel)):
- # TODO: remove when deprecating Pydantic v1
- encoders: dict[Any, Any] = {}
- if isinstance(obj, may_v1.BaseModel):
- encoders = getattr(obj.__config__, "json_encoders", {})
- if custom_encoder:
- encoders = {**encoders, **custom_encoder}
- obj_dict = _model_dump(
- obj, # type: ignore[arg-type]
+ if isinstance(obj, BaseModel):
+ obj_dict = obj.model_dump(
mode="json",
include=include,
exclude=exclude,
exclude_none=exclude_none,
exclude_defaults=exclude_defaults,
)
- if "__root__" in obj_dict:
- obj_dict = obj_dict["__root__"]
return jsonable_encoder(
obj_dict,
exclude_none=exclude_none,
exclude_defaults=exclude_defaults,
- # TODO: remove when deprecating Pydantic v1
- custom_encoder=encoders,
sqlalchemy_safe=sqlalchemy_safe,
)
if dataclasses.is_dataclass(obj):
return str(obj)
if isinstance(obj, (str, int, float, type(None))):
return obj
- if _is_undefined(obj):
+ if isinstance(obj, PydanticUndefinedType):
return None
if isinstance(obj, dict):
encoded_dict = {}
for encoder, classes_tuple in encoders_by_class_tuples.items():
if isinstance(obj, classes_tuple):
return encoder(obj)
-
+ if is_pydantic_v1_model_instance(obj):
+ raise PydanticV1NotSupportedError(
+ "pydantic.v1 models are no longer supported by FastAPI."
+ f" Please update the model {obj!r}."
+ )
try:
data = dict(obj)
except Exception as e:
self.body = body
+class PydanticV1NotSupportedError(FastAPIError):
+ """
+ A pydantic.v1 model is used, which is no longer supported.
+ """
+
+
class FastAPIDeprecationWarning(UserWarning):
"""
A custom deprecation warning as DeprecationWarning is ignored
-from collections.abc import Iterable
+from collections.abc import Iterable, Mapping
from enum import Enum
from typing import Annotated, Any, Callable, Optional, Union
-from fastapi._compat import (
- CoreSchema,
+from fastapi._compat import with_info_plain_validator_function
+from fastapi.logger import logger
+from pydantic import (
+ AnyUrl,
+ BaseModel,
+ Field,
GetJsonSchemaHandler,
- JsonSchemaValue,
- with_info_plain_validator_function,
)
-from fastapi.logger import logger
-from pydantic import AnyUrl, BaseModel, Field
from typing_extensions import Literal, TypedDict
from typing_extensions import deprecated as typing_deprecated
@classmethod
def __get_pydantic_json_schema__(
- cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
- ) -> JsonSchemaValue:
+ cls, core_schema: Mapping[str, Any], handler: GetJsonSchemaHandler
+ ) -> dict[str, Any]:
return {"type": "string", "format": "email"}
@classmethod
def __get_pydantic_core_schema__(
- cls, source: type[Any], handler: Callable[[Any], CoreSchema]
- ) -> CoreSchema:
+ cls, source: type[Any], handler: Callable[[Any], Mapping[str, Any]]
+ ) -> Mapping[str, Any]:
return with_info_plain_validator_function(cls._validate)
from fastapi import routing
from fastapi._compat import (
- JsonSchemaValue,
ModelField,
Undefined,
get_compat_model_name_map,
from starlette.routing import BaseRoute
from typing_extensions import Literal
-from .._compat import _is_model_field
-
validation_error_definition = {
"title": "ValidationError",
"type": "object",
dependant: Dependant,
model_name_map: ModelNameMap,
field_mapping: dict[
- tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
+ tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any]
],
separate_input_output_schemas: bool = True,
) -> list[dict[str, Any]]:
body_field: Optional[ModelField],
model_name_map: ModelNameMap,
field_mapping: dict[
- tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
+ tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any]
],
separate_input_output_schemas: bool = True,
) -> Optional[dict[str, Any]]:
if not body_field:
return None
- assert _is_model_field(body_field)
+ assert isinstance(body_field, ModelField)
body_schema = get_schema_from_model_field(
field=body_field,
model_name_map=model_name_map,
operation_ids: set[str],
model_name_map: ModelNameMap,
field_mapping: dict[
- tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
+ tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any]
],
separate_input_output_schemas: bool = True,
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
route, routing.APIRoute
):
if route.body_field:
- assert _is_model_field(route.body_field), (
+ assert isinstance(route.body_field, ModelField), (
"A request body must be a Pydantic Field"
)
body_fields_from_routes.append(route.body_field)
from fastapi import params
from fastapi._compat import Undefined
from fastapi.openapi.models import Example
+from pydantic import AliasChoices, AliasPath
from typing_extensions import Literal, deprecated
_Unset: Any = Undefined
"""
),
] = _Unset,
- # TODO: update when deprecating Pydantic v1, import these types
- # validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Annotated[
- Union[str, None],
+ Union[str, AliasPath, AliasChoices, None],
Doc(
"""
'Whitelist' validation step. The parameter field will be the single one
"""
),
] = _Unset,
- # TODO: update when deprecating Pydantic v1, import these types
- # validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Annotated[
- Union[str, None],
+ Union[str, AliasPath, AliasChoices, None],
Doc(
"""
'Whitelist' validation step. The parameter field will be the single one
"""
),
] = _Unset,
- # TODO: update when deprecating Pydantic v1, import these types
- # validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Annotated[
- Union[str, None],
+ Union[str, AliasPath, AliasChoices, None],
Doc(
"""
'Whitelist' validation step. The parameter field will be the single one
"""
),
] = _Unset,
- # TODO: update when deprecating Pydantic v1, import these types
- # validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Annotated[
- Union[str, None],
+ Union[str, AliasPath, AliasChoices, None],
Doc(
"""
'Whitelist' validation step. The parameter field will be the single one
"""
),
] = _Unset,
- # TODO: update when deprecating Pydantic v1, import these types
- # validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Annotated[
- Union[str, None],
+ Union[str, AliasPath, AliasChoices, None],
Doc(
"""
'Whitelist' validation step. The parameter field will be the single one
"""
),
] = _Unset,
- # TODO: update when deprecating Pydantic v1, import these types
- # validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Annotated[
- Union[str, None],
+ Union[str, AliasPath, AliasChoices, None],
Doc(
"""
'Whitelist' validation step. The parameter field will be the single one
"""
),
] = _Unset,
- # TODO: update when deprecating Pydantic v1, import these types
- # validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Annotated[
- Union[str, None],
+ Union[str, AliasPath, AliasChoices, None],
Doc(
"""
'Whitelist' validation step. The parameter field will be the single one
from fastapi.exceptions import FastAPIDeprecationWarning
from fastapi.openapi.models import Example
+from pydantic import AliasChoices, AliasPath
from pydantic.fields import FieldInfo
from typing_extensions import Literal, deprecated
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,
+ validation_alias: Union[str, AliasPath, AliasChoices, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
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,
+ validation_alias: Union[str, AliasPath, AliasChoices, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
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,
+ validation_alias: Union[str, AliasPath, AliasChoices, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
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,
+ validation_alias: Union[str, AliasPath, AliasChoices, None] = None,
serialization_alias: Union[str, None] = None,
convert_underscores: bool = True,
title: Optional[str] = None,
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,
+ validation_alias: Union[str, AliasPath, AliasChoices, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = 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,
+ validation_alias: Union[str, AliasPath, AliasChoices, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = 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,
+ validation_alias: Union[str, AliasPath, AliasChoices, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = 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,
+ validation_alias: Union[str, AliasPath, AliasChoices, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
import functools
import inspect
import json
-import warnings
from collections.abc import (
AsyncIterator,
Awaitable,
)
from annotated_doc import Doc
-from fastapi import params, temp_pydantic_v1_params
+from fastapi import params
from fastapi._compat import (
ModelField,
Undefined,
- _get_model_config,
- _model_dump,
- _normalize_errors,
annotation_is_pydantic_v1,
lenient_issubclass,
- may_v1,
)
from fastapi.datastructures import Default, DefaultPlaceholder
from fastapi.dependencies.models import Dependant
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import (
EndpointContext,
- FastAPIDeprecationWarning,
FastAPIError,
+ PydanticV1NotSupportedError,
RequestValidationError,
ResponseValidationError,
WebSocketRequestValidationError,
return app
-def _prepare_response_content(
- res: Any,
- *,
- exclude_unset: bool,
- exclude_defaults: bool = False,
- exclude_none: bool = False,
-) -> Any:
- if isinstance(res, may_v1.BaseModel):
- read_with_orm_mode = getattr(_get_model_config(res), "read_with_orm_mode", None) # type: ignore[arg-type]
- if read_with_orm_mode:
- # Let from_orm extract the data from this model instead of converting
- # it now to a dict.
- # Otherwise, there's no way to extract lazy data that requires attribute
- # access instead of dict iteration, e.g. lazy relationships.
- return res
- return _model_dump(
- res, # type: ignore[arg-type]
- by_alias=True,
- exclude_unset=exclude_unset,
- exclude_defaults=exclude_defaults,
- exclude_none=exclude_none,
- )
- elif isinstance(res, list):
- return [
- _prepare_response_content(
- item,
- exclude_unset=exclude_unset,
- exclude_defaults=exclude_defaults,
- exclude_none=exclude_none,
- )
- for item in res
- ]
- elif isinstance(res, dict):
- return {
- k: _prepare_response_content(
- v,
- exclude_unset=exclude_unset,
- exclude_defaults=exclude_defaults,
- exclude_none=exclude_none,
- )
- for k, v in res.items()
- }
- return res
-
-
def _merge_lifespan_context(
original_context: Lifespan[Any], nested_context: Lifespan[Any]
) -> Lifespan[Any]:
) -> Any:
if field:
errors = []
- if not hasattr(field, "serialize"):
- # pydantic v1
- response_content = _prepare_response_content(
- response_content,
- exclude_unset=exclude_unset,
- exclude_defaults=exclude_defaults,
- exclude_none=exclude_none,
- )
if is_coroutine:
value, errors_ = field.validate(response_content, {}, loc=("response",))
else:
)
if isinstance(errors_, list):
errors.extend(errors_)
- elif errors_:
- errors.append(errors_)
if errors:
ctx = endpoint_ctx or EndpointContext()
raise ResponseValidationError(
- errors=_normalize_errors(errors),
+ errors=errors,
body=response_content,
endpoint_ctx=ctx,
)
- if hasattr(field, "serialize"):
- return field.serialize(
- value,
- include=include,
- exclude=exclude,
- by_alias=by_alias,
- exclude_unset=exclude_unset,
- exclude_defaults=exclude_defaults,
- exclude_none=exclude_none,
- )
-
- return jsonable_encoder(
+ return field.serialize(
value,
include=include,
exclude=exclude,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
+
else:
return jsonable_encoder(response_content)
) -> Callable[[Request], Coroutine[Any, Any, Response]]:
assert dependant.call is not None, "dependant.call must be a function"
is_coroutine = dependant.is_coroutine_callable
- is_body_form = body_field and isinstance(
- body_field.field_info, (params.Form, temp_pydantic_v1_params.Form)
- )
+ is_body_form = body_field and isinstance(body_field.field_info, params.Form)
if isinstance(response_class, DefaultPlaceholder):
actual_response_class: type[Response] = response_class.value
else:
response.headers.raw.extend(solved_result.response.headers.raw)
if errors:
validation_error = RequestValidationError(
- _normalize_errors(errors), body=body, endpoint_ctx=endpoint_ctx
+ errors, body=body, endpoint_ctx=endpoint_ctx
)
raise validation_error
)
if solved_result.errors:
raise WebSocketRequestValidationError(
- _normalize_errors(solved_result.errors),
+ solved_result.errors,
endpoint_ctx=endpoint_ctx,
)
assert dependant.call is not None, "dependant.call must be a function"
)
response_name = "Response_" + self.unique_id
if annotation_is_pydantic_v1(self.response_model):
- warnings.warn(
- "pydantic.v1 is deprecated and will soon stop being supported by FastAPI."
- f" Please update the response model {self.response_model!r}.",
- category=FastAPIDeprecationWarning,
- stacklevel=4,
+ raise PydanticV1NotSupportedError(
+ "pydantic.v1 models are no longer supported by FastAPI."
+ f" Please update the response model {self.response_model!r}."
)
self.response_field = create_model_field(
name=response_name,
)
response_name = f"Response_{additional_status_code}_{self.unique_id}"
if annotation_is_pydantic_v1(model):
- warnings.warn(
- "pydantic.v1 is deprecated and will soon stop being supported by FastAPI."
- f" In responses={{}}, please update {model}.",
- category=FastAPIDeprecationWarning,
- stacklevel=4,
+ raise PydanticV1NotSupportedError(
+ "pydantic.v1 models are no longer supported by FastAPI."
+ f" In responses={{}}, please update {model}."
)
response_field = create_model_field(
name=response_name, type_=model, mode="serialization"
+++ /dev/null
-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,
- )
Any,
Optional,
Union,
- cast,
)
from weakref import WeakKeyDictionary
UndefinedType,
Validator,
annotation_is_pydantic_v1,
- lenient_issubclass,
- may_v1,
)
from fastapi.datastructures import DefaultPlaceholder, DefaultType
-from fastapi.exceptions import FastAPIDeprecationWarning
+from fastapi.exceptions import FastAPIDeprecationWarning, PydanticV1NotSupportedError
from pydantic import BaseModel
from pydantic.fields import FieldInfo
from typing_extensions import Literal
mode: Literal["validation", "serialization"] = "validation",
version: Literal["1", "auto"] = "auto",
) -> ModelField:
- class_validators = class_validators or {}
-
- v1_model_config = may_v1.BaseConfig
- v1_field_info = field_info or may_v1.FieldInfo()
- v1_kwargs = {
- "name": name,
- "field_info": v1_field_info,
- "type_": type_,
- "class_validators": class_validators,
- "default": default,
- "required": required,
- "model_config": v1_model_config,
- "alias": alias,
- }
-
- if (
- annotation_is_pydantic_v1(type_)
- or isinstance(field_info, may_v1.FieldInfo)
- or version == "1"
- ):
- from fastapi._compat import v1
-
- try:
- return v1.ModelField(**v1_kwargs) # type: ignore[return-value]
- except RuntimeError:
- raise fastapi.exceptions.FastAPIError(
- _invalid_args_message.format(type_=type_)
- ) from None
- else:
- field_info = field_info or FieldInfo(
- annotation=type_, default=default, alias=alias
+ if annotation_is_pydantic_v1(type_):
+ raise PydanticV1NotSupportedError(
+ "pydantic.v1 models are no longer supported by FastAPI."
+ f" Please update the response model {type_!r}."
)
- kwargs = {"mode": mode, "name": name, "field_info": field_info}
- try:
- return v2.ModelField(**kwargs) # type: ignore[return-value,arg-type]
- except PydanticSchemaGenerationError:
- raise fastapi.exceptions.FastAPIError(
- _invalid_args_message.format(type_=type_)
- ) from None
- # Pydantic v2 is not installed, but it's not a Pydantic v1 ModelField, it could be
- # a Pydantic v1 type, like a constrained int
- from fastapi._compat import v1
+ class_validators = class_validators or {}
+ field_info = field_info or FieldInfo(annotation=type_, default=default, alias=alias)
+ kwargs = {"mode": mode, "name": name, "field_info": field_info}
try:
- return v1.ModelField(**v1_kwargs)
- except RuntimeError:
+ return v2.ModelField(**kwargs) # type: ignore[return-value,arg-type]
+ except PydanticSchemaGenerationError:
raise fastapi.exceptions.FastAPIError(
_invalid_args_message.format(type_=type_)
) from None
*,
cloned_types: Optional[MutableMapping[type[BaseModel], type[BaseModel]]] = None,
) -> ModelField:
- if isinstance(field, v2.ModelField):
- return field
-
- from fastapi._compat import v1
-
- # cloned_types caches already cloned types to support recursive models and improve
- # performance by avoiding unnecessary cloning
- if cloned_types is None:
- cloned_types = _CLONED_TYPES_CACHE
-
- original_type = field.type_
- use_type = original_type
- if lenient_issubclass(original_type, v1.BaseModel):
- original_type = cast(type[v1.BaseModel], original_type)
- use_type = cloned_types.get(original_type)
- if use_type is None:
- use_type = v1.create_model(original_type.__name__, __base__=original_type)
- cloned_types[original_type] = use_type
- for f in original_type.__fields__.values():
- use_type.__fields__[f.name] = create_cloned_field(
- f,
- cloned_types=cloned_types,
- )
- new_field = create_model_field(name=field.name, type_=use_type, version="1")
- new_field.has_alias = field.has_alias # type: ignore[attr-defined]
- new_field.alias = field.alias # type: ignore[misc]
- new_field.class_validators = field.class_validators # type: ignore[attr-defined]
- new_field.default = field.default # type: ignore[misc]
- new_field.default_factory = field.default_factory # type: ignore[attr-defined]
- new_field.required = field.required # type: ignore[misc]
- new_field.model_config = field.model_config # type: ignore[attr-defined]
- new_field.field_info = field.field_info
- new_field.allow_none = field.allow_none # type: ignore[attr-defined]
- new_field.validate_always = field.validate_always # type: ignore[attr-defined]
- if field.sub_fields: # type: ignore[attr-defined]
- new_field.sub_fields = [ # type: ignore[attr-defined]
- create_cloned_field(sub_field, cloned_types=cloned_types)
- for sub_field in field.sub_fields # type: ignore[attr-defined]
- ]
- if field.key_field: # type: ignore[attr-defined]
- new_field.key_field = create_cloned_field( # type: ignore[attr-defined]
- field.key_field, # type: ignore[attr-defined]
- cloned_types=cloned_types,
- )
- new_field.validators = field.validators # type: ignore[attr-defined]
- new_field.pre_validators = field.pre_validators # type: ignore[attr-defined]
- new_field.post_validators = field.post_validators # type: ignore[attr-defined]
- new_field.parse_json = field.parse_json # type: ignore[attr-defined]
- new_field.shape = field.shape # type: ignore[attr-defined]
- new_field.populate_validators() # type: ignore[attr-defined]
- return new_field
+ return field
def generate_operation_id_for_path(
"docs_src/dependencies/tutorial008_an_py39.py", # difficult to mock
"docs_src/dependencies/tutorial013_an_py310.py", # temporary code example?
"docs_src/dependencies/tutorial014_an_py310.py", # temporary code example?
- # Pydantic V1
+ # Pydantic v1 migration, no longer tested
+ "docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py",
+ "docs_src/pydantic_v1_in_v2/tutorial001_an_py39.py",
+ "docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py",
+ "docs_src/pydantic_v1_in_v2/tutorial002_an_py39.py",
+ "docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py",
+ "docs_src/pydantic_v1_in_v2/tutorial003_an_py39.py",
+ "docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py",
+ "docs_src/pydantic_v1_in_v2/tutorial004_an_py39.py",
+ # TODO: remove when removing this file, after updating translations, Pydantic v1
"docs_src/schema_extra_example/tutorial001_pv1_py310.py",
- "docs_src/query_param_models/tutorial002_pv1_py310.py",
- "docs_src/query_param_models/tutorial002_pv1_an_py310.py",
- "docs_src/header_param_models/tutorial002_pv1_py310.py",
- "docs_src/header_param_models/tutorial002_pv1_an_py310.py",
- "docs_src/cookie_param_models/tutorial002_pv1_py310.py",
- "docs_src/cookie_param_models/tutorial002_pv1_an_py310.py",
+ "docs_src/schema_extra_example/tutorial001_pv1_py39.py",
+ "docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py",
+ "docs_src/settings/app03_py39/config_pv1.py",
+ "docs_src/settings/app03_an_py39/config_pv1.py",
+ "docs_src/settings/tutorial001_pv1_py39.py",
]
[tool.coverage.report]
-from typing import Any, Union
+from typing import Union
from fastapi import FastAPI, UploadFile
from fastapi._compat import (
Undefined,
- _get_model_config,
- get_cached_model_fields,
- is_scalar_field,
is_uploadfile_sequence_annotation,
- may_v1,
)
from fastapi._compat.shared import is_bytes_sequence_annotation
from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict
from pydantic.fields import FieldInfo
-from .utils import needs_py310, needs_py_lt_314
+from .utils import needs_py310
def test_model_field_default_required():
assert field.default is Undefined
-@needs_py_lt_314
-def test_v1_plain_validator_function():
- from fastapi._compat import v1
-
- # For coverage
- def func(v): # pragma: no cover
- return v
-
- result = v1.with_info_plain_validator_function(func)
- assert result == {}
-
-
-def test_is_model_field():
- # For coverage
- from fastapi._compat import _is_model_field
-
- assert not _is_model_field(str)
-
-
-def test_get_model_config():
- # For coverage in Pydantic v2
- class Foo(BaseModel):
- model_config = ConfigDict(from_attributes=True)
-
- foo = Foo()
- config = _get_model_config(foo)
- assert config == {"from_attributes": True}
-
-
def test_complex():
app = FastAPI()
result = v2.serialize_sequence_value(field=field, value=["x", "y"])
assert result == ["x", "y"]
assert isinstance(result, list)
-
-
-@needs_py_lt_314
-def test_is_pv1_scalar_field():
- from fastapi._compat import v1
-
- # For coverage
- class Model(v1.BaseModel):
- foo: Union[str, dict[str, Any]]
-
- fields = v1.get_model_fields(Model)
- assert not is_scalar_field(fields[0])
-
-
-@needs_py_lt_314
-def test_get_model_fields_cached():
- from fastapi._compat import v1
-
- class Model(may_v1.BaseModel):
- foo: str
-
- non_cached_fields = v1.get_model_fields(Model)
- non_cached_fields2 = v1.get_model_fields(Model)
- cached_fields = get_cached_model_fields(Model)
- cached_fields2 = get_cached_model_fields(Model)
- for f1, f2 in zip(cached_fields, cached_fields2):
- assert f1 is f2
-
- assert non_cached_fields is not non_cached_fields2
- assert cached_fields is cached_fields2
+++ /dev/null
-import sys
-import warnings
-from typing import Optional
-
-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 typing import Annotated
-
-from fastapi import FastAPI
-from fastapi._compat.v1 import BaseModel
-from fastapi.temp_pydantic_v1_params import (
- Body,
- Cookie,
- File,
- Form,
- Header,
- Path,
- Query,
-)
-from fastapi.testclient import TestClient
-from inline_snapshot import snapshot
-
-
-class Item(BaseModel):
- name: str
- price: float
- description: Optional[str] = None
-
-
-app = FastAPI()
-
-with warnings.catch_warnings(record=True):
- warnings.simplefilter("always")
-
- @app.get("/items/{item_id}")
- def get_item_with_path(
- item_id: Annotated[int, Path(title="The ID of the item", ge=1, le=1000)],
- ):
- return {"item_id": item_id}
-
- @app.get("/items/")
- def get_items_with_query(
- q: Annotated[
- Optional[str],
- Query(min_length=3, max_length=50, pattern="^[a-zA-Z0-9 ]+$"),
- ] = None,
- skip: Annotated[int, Query(ge=0)] = 0,
- limit: Annotated[int, Query(ge=1, le=100, examples=[5])] = 10,
- ):
- return {"q": q, "skip": skip, "limit": limit}
-
- @app.get("/users/")
- def get_user_with_header(
- x_custom: Annotated[Optional[str], Header()] = None,
- x_token: Annotated[Optional[str], Header(convert_underscores=True)] = None,
- ):
- return {"x_custom": x_custom, "x_token": x_token}
-
- @app.get("/cookies/")
- def get_cookies(
- session_id: Annotated[Optional[str], Cookie()] = None,
- tracking_id: Annotated[Optional[str], Cookie(min_length=10)] = None,
- ):
- return {"session_id": session_id, "tracking_id": tracking_id}
-
- @app.post("/items/")
- def create_item(
- item: Annotated[
- Item,
- Body(
- examples=[{"name": "Foo", "price": 35.4, "description": "The Foo item"}]
- ),
- ],
- ):
- return {"item": item}
-
- @app.post("/items-embed/")
- def create_item_embed(
- item: Annotated[Item, Body(embed=True)],
- ):
- return {"item": item}
-
- @app.put("/items/{item_id}")
- def update_item(
- item_id: Annotated[int, Path(ge=1)],
- item: Annotated[Item, Body()],
- importance: Annotated[int, Body(gt=0, le=10)],
- ):
- return {"item": item, "importance": importance}
-
- @app.post("/form-data/")
- def submit_form(
- username: Annotated[str, Form(min_length=3, max_length=50)],
- password: Annotated[str, Form(min_length=8)],
- email: Annotated[Optional[str], Form()] = None,
- ):
- return {"username": username, "password": password, "email": email}
-
- @app.post("/upload/")
- def upload_file(
- file: Annotated[bytes, File()],
- description: Annotated[Optional[str], Form()] = None,
- ):
- return {"file_size": len(file), "description": description}
-
- @app.post("/upload-multiple/")
- def upload_multiple_files(
- files: Annotated[list[bytes], File()],
- note: Annotated[str, Form()] = "",
- ):
- return {
- "file_count": len(files),
- "total_size": sum(len(f) for f in files),
- "note": note,
- }
-
-
-client = TestClient(app)
-
-
-# Path parameter tests
-def test_path_param_valid():
- response = client.get("/items/50")
- assert response.status_code == 200
- assert response.json() == {"item_id": 50}
-
-
-def test_path_param_too_large():
- response = client.get("/items/1001")
- assert response.status_code == 422
- error = response.json()["detail"][0]
- assert error["loc"] == ["path", "item_id"]
-
-
-def test_path_param_too_small():
- response = client.get("/items/0")
- assert response.status_code == 422
- error = response.json()["detail"][0]
- assert error["loc"] == ["path", "item_id"]
-
-
-# Query parameter tests
-def test_query_params_valid():
- response = client.get("/items/?q=test search&skip=5&limit=20")
- assert response.status_code == 200
- assert response.json() == {"q": "test search", "skip": 5, "limit": 20}
-
-
-def test_query_params_defaults():
- response = client.get("/items/")
- assert response.status_code == 200
- assert response.json() == {"q": None, "skip": 0, "limit": 10}
-
-
-def test_query_param_too_short():
- response = client.get("/items/?q=ab")
- assert response.status_code == 422
- error = response.json()["detail"][0]
- assert error["loc"] == ["query", "q"]
-
-
-def test_query_param_invalid_pattern():
- response = client.get("/items/?q=test@#$")
- assert response.status_code == 422
- error = response.json()["detail"][0]
- assert error["loc"] == ["query", "q"]
-
-
-def test_query_param_limit_too_large():
- response = client.get("/items/?limit=101")
- assert response.status_code == 422
- error = response.json()["detail"][0]
- assert error["loc"] == ["query", "limit"]
-
-
-# Header parameter tests
-def test_header_params():
- response = client.get(
- "/users/",
- headers={"X-Custom": "Plumbus", "X-Token": "secret-token"},
- )
- assert response.status_code == 200
- assert response.json() == {
- "x_custom": "Plumbus",
- "x_token": "secret-token",
- }
-
-
-def test_header_underscore_conversion():
- response = client.get(
- "/users/",
- headers={"x-token": "secret-token-with-dash"},
- )
- assert response.status_code == 200
- assert response.json()["x_token"] == "secret-token-with-dash"
-
-
-def test_header_params_none():
- response = client.get("/users/")
- assert response.status_code == 200
- assert response.json() == {"x_custom": None, "x_token": None}
-
-
-# Cookie parameter tests
-def test_cookie_params():
- with TestClient(app) as test_client:
- test_client.cookies.set("session_id", "abc123")
- test_client.cookies.set("tracking_id", "1234567890abcdef")
- response = test_client.get("/cookies/")
- assert response.status_code == 200
- assert response.json() == {
- "session_id": "abc123",
- "tracking_id": "1234567890abcdef",
- }
-
-
-def test_cookie_tracking_id_too_short():
- with TestClient(app) as test_client:
- test_client.cookies.set("tracking_id", "short")
- response = test_client.get("/cookies/")
- assert response.status_code == 422
- assert response.json() == snapshot(
- {
- "detail": [
- {
- "loc": ["cookie", "tracking_id"],
- "msg": "ensure this value has at least 10 characters",
- "type": "value_error.any_str.min_length",
- "ctx": {"limit_value": 10},
- }
- ]
- }
- )
-
-
-def test_cookie_params_none():
- response = client.get("/cookies/")
- assert response.status_code == 200
- assert response.json() == {"session_id": None, "tracking_id": None}
-
-
-# Body parameter tests
-def test_body_param():
- response = client.post(
- "/items/",
- json={"name": "Test Item", "price": 29.99, "description": "A test item"},
- )
- assert response.status_code == 200
- assert response.json() == {
- "item": {
- "name": "Test Item",
- "price": 29.99,
- "description": "A test item",
- }
- }
-
-
-def test_body_param_minimal():
- response = client.post(
- "/items/",
- json={"name": "Minimal", "price": 9.99},
- )
- assert response.status_code == 200
- assert response.json() == {
- "item": {"name": "Minimal", "price": 9.99, "description": None}
- }
-
-
-def test_body_param_missing_required():
- response = client.post(
- "/items/",
- json={"name": "Incomplete"},
- )
- assert response.status_code == 422
- error = response.json()["detail"][0]
- assert error["loc"] == ["body", "price"]
-
-
-def test_body_embed():
- response = client.post(
- "/items-embed/",
- json={"item": {"name": "Embedded", "price": 15.0}},
- )
- assert response.status_code == 200
- assert response.json() == {
- "item": {"name": "Embedded", "price": 15.0, "description": None}
- }
-
-
-def test_body_embed_wrong_structure():
- response = client.post(
- "/items-embed/",
- json={"name": "Not Embedded", "price": 15.0},
- )
- assert response.status_code == 422
-
-
-# Multiple body parameters test
-def test_multiple_body_params():
- response = client.put(
- "/items/5",
- json={
- "item": {"name": "Updated Item", "price": 49.99},
- "importance": 8,
- },
- )
- assert response.status_code == 200
- assert response.json() == snapshot(
- {
- "item": {"name": "Updated Item", "price": 49.99, "description": None},
- "importance": 8,
- }
- )
-
-
-def test_multiple_body_params_importance_too_large():
- response = client.put(
- "/items/5",
- json={
- "item": {"name": "Item", "price": 10.0},
- "importance": 11,
- },
- )
- assert response.status_code == 422
- assert response.json() == snapshot(
- {
- "detail": [
- {
- "loc": ["body", "importance"],
- "msg": "ensure this value is less than or equal to 10",
- "type": "value_error.number.not_le",
- "ctx": {"limit_value": 10},
- }
- ]
- }
- )
-
-
-def test_multiple_body_params_importance_too_small():
- response = client.put(
- "/items/5",
- json={
- "item": {"name": "Item", "price": 10.0},
- "importance": 0,
- },
- )
- assert response.status_code == 422
- assert response.json() == snapshot(
- {
- "detail": [
- {
- "loc": ["body", "importance"],
- "msg": "ensure this value is greater than 0",
- "type": "value_error.number.not_gt",
- "ctx": {"limit_value": 0},
- }
- ]
- }
- )
-
-
-# Form parameter tests
-def test_form_data_valid():
- response = client.post(
- "/form-data/",
- data={
- "username": "testuser",
- "password": "password123",
- "email": "test@example.com",
- },
- )
- assert response.status_code == 200, response.text
- assert response.json() == {
- "username": "testuser",
- "password": "password123",
- "email": "test@example.com",
- }
-
-
-def test_form_data_optional_field():
- response = client.post(
- "/form-data/",
- data={"username": "testuser", "password": "password123"},
- )
- assert response.status_code == 200
- assert response.json() == {
- "username": "testuser",
- "password": "password123",
- "email": None,
- }
-
-
-def test_form_data_username_too_short():
- response = client.post(
- "/form-data/",
- data={"username": "ab", "password": "password123"},
- )
- assert response.status_code == 422
- assert response.json() == snapshot(
- {
- "detail": [
- {
- "loc": ["body", "username"],
- "msg": "ensure this value has at least 3 characters",
- "type": "value_error.any_str.min_length",
- "ctx": {"limit_value": 3},
- }
- ]
- }
- )
-
-
-def test_form_data_password_too_short():
- response = client.post(
- "/form-data/",
- data={"username": "testuser", "password": "short"},
- )
- assert response.status_code == 422
- assert response.json() == snapshot(
- {
- "detail": [
- {
- "loc": ["body", "password"],
- "msg": "ensure this value has at least 8 characters",
- "type": "value_error.any_str.min_length",
- "ctx": {"limit_value": 8},
- }
- ]
- }
- )
-
-
-# File upload tests
-def test_upload_file():
- response = client.post(
- "/upload/",
- files={"file": ("test.txt", b"Hello, World!", "text/plain")},
- data={"description": "A test file"},
- )
- assert response.status_code == 200
- assert response.json() == {
- "file_size": 13,
- "description": "A test file",
- }
-
-
-def test_upload_file_without_description():
- response = client.post(
- "/upload/",
- files={"file": ("test.txt", b"Hello!", "text/plain")},
- )
- assert response.status_code == 200
- assert response.json() == {
- "file_size": 6,
- "description": None,
- }
-
-
-def test_upload_multiple_files():
- response = client.post(
- "/upload-multiple/",
- files=[
- ("files", ("file1.txt", b"Content 1", "text/plain")),
- ("files", ("file2.txt", b"Content 2", "text/plain")),
- ("files", ("file3.txt", b"Content 3", "text/plain")),
- ],
- data={"note": "Multiple files uploaded"},
- )
- assert response.status_code == 200
- assert response.json() == {
- "file_count": 3,
- "total_size": 27,
- "note": "Multiple files uploaded",
- }
-
-
-def test_upload_multiple_files_empty_note():
- response = client.post(
- "/upload-multiple/",
- files=[
- ("files", ("file1.txt", b"Test", "text/plain")),
- ],
- )
- assert response.status_code == 200
- assert response.json()["file_count"] == 1
- assert response.json()["note"] == ""
-
-
-# __repr__ tests
-def test_query_repr():
- query_param = Query(default=None, min_length=3)
- assert repr(query_param) == "Query(None)"
-
-
-def test_body_repr():
- body_param = Body(default=None)
- assert repr(body_param) == "Body(None)"
-
-
-# Deprecation warning tests for regex parameter
-def test_query_regex_deprecation_warning():
- with pytest.warns(FastAPIDeprecationWarning, match="`regex` has been deprecated"):
- Query(regex="^test$")
-
-
-def test_body_regex_deprecation_warning():
- with pytest.warns(FastAPIDeprecationWarning, match="`regex` has been deprecated"):
- Body(regex="^test$")
-
-
-# Deprecation warning tests for example parameter
-def test_query_example_deprecation_warning():
- with pytest.warns(FastAPIDeprecationWarning, match="`example` has been deprecated"):
- Query(example="test example")
-
-
-def test_body_example_deprecation_warning():
- with pytest.warns(FastAPIDeprecationWarning, match="`example` has been deprecated"):
- Body(example={"test": "example"})
-
-
-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": {
- "/items/{item_id}": {
- "get": {
- "summary": "Get Item With Path",
- "operationId": "get_item_with_path_items__item_id__get",
- "parameters": [
- {
- "name": "item_id",
- "in": "path",
- "required": True,
- "schema": {
- "title": "The ID of the item",
- "minimum": 1,
- "maximum": 1000,
- "type": "integer",
- },
- }
- ],
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {"application/json": {"schema": {}}},
- },
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- },
- },
- },
- },
- "put": {
- "summary": "Update Item",
- "operationId": "update_item_items__item_id__put",
- "parameters": [
- {
- "name": "item_id",
- "in": "path",
- "required": True,
- "schema": {
- "title": "Item Id",
- "minimum": 1,
- "type": "integer",
- },
- }
- ],
- "requestBody": {
- "required": True,
- "content": {
- "application/json": {
- "schema": {
- "title": "Body",
- "allOf": [
- {
- "$ref": "#/components/schemas/Body_update_item_items__item_id__put"
- }
- ],
- }
- }
- },
- },
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {"application/json": {"schema": {}}},
- },
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- },
- },
- },
- },
- },
- "/items/": {
- "get": {
- "summary": "Get Items With Query",
- "operationId": "get_items_with_query_items__get",
- "parameters": [
- {
- "name": "q",
- "in": "query",
- "required": False,
- "schema": {
- "title": "Q",
- "maxLength": 50,
- "minLength": 3,
- "pattern": "^[a-zA-Z0-9 ]+$",
- "type": "string",
- },
- },
- {
- "name": "skip",
- "in": "query",
- "required": False,
- "schema": {
- "title": "Skip",
- "default": 0,
- "minimum": 0,
- "type": "integer",
- },
- },
- {
- "name": "limit",
- "in": "query",
- "required": False,
- "schema": {
- "title": "Limit",
- "default": 10,
- "minimum": 1,
- "maximum": 100,
- "examples": [5],
- "type": "integer",
- },
- },
- ],
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {"application/json": {"schema": {}}},
- },
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- },
- },
- },
- },
- "post": {
- "summary": "Create Item",
- "operationId": "create_item_items__post",
- "requestBody": {
- "required": True,
- "content": {
- "application/json": {
- "schema": {
- "title": "Item",
- "examples": [
- {
- "name": "Foo",
- "price": 35.4,
- "description": "The Foo item",
- }
- ],
- "allOf": [
- {"$ref": "#/components/schemas/Item"}
- ],
- }
- }
- },
- },
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {"application/json": {"schema": {}}},
- },
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- },
- },
- },
- },
- },
- "/users/": {
- "get": {
- "summary": "Get User With Header",
- "operationId": "get_user_with_header_users__get",
- "parameters": [
- {
- "name": "x-custom",
- "in": "header",
- "required": False,
- "schema": {"title": "X-Custom", "type": "string"},
- },
- {
- "name": "x-token",
- "in": "header",
- "required": False,
- "schema": {"title": "X-Token", "type": "string"},
- },
- ],
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {"application/json": {"schema": {}}},
- },
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- },
- },
- },
- }
- },
- "/cookies/": {
- "get": {
- "summary": "Get Cookies",
- "operationId": "get_cookies_cookies__get",
- "parameters": [
- {
- "name": "session_id",
- "in": "cookie",
- "required": False,
- "schema": {"title": "Session Id", "type": "string"},
- },
- {
- "name": "tracking_id",
- "in": "cookie",
- "required": False,
- "schema": {
- "title": "Tracking Id",
- "minLength": 10,
- "type": "string",
- },
- },
- ],
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {"application/json": {"schema": {}}},
- },
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- },
- },
- },
- }
- },
- "/items-embed/": {
- "post": {
- "summary": "Create Item Embed",
- "operationId": "create_item_embed_items_embed__post",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "allOf": [
- {
- "$ref": "#/components/schemas/Body_create_item_embed_items_embed__post"
- }
- ],
- "title": "Body",
- }
- }
- },
- "required": True,
- },
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {"application/json": {"schema": {}}},
- },
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- },
- },
- },
- }
- },
- "/form-data/": {
- "post": {
- "summary": "Submit Form",
- "operationId": "submit_form_form_data__post",
- "requestBody": {
- "content": {
- "application/x-www-form-urlencoded": {
- "schema": {
- "allOf": [
- {
- "$ref": "#/components/schemas/Body_submit_form_form_data__post"
- }
- ],
- "title": "Body",
- }
- }
- },
- "required": True,
- },
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {"application/json": {"schema": {}}},
- },
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- },
- },
- },
- }
- },
- "/upload/": {
- "post": {
- "summary": "Upload File",
- "operationId": "upload_file_upload__post",
- "requestBody": {
- "content": {
- "multipart/form-data": {
- "schema": {
- "allOf": [
- {
- "$ref": "#/components/schemas/Body_upload_file_upload__post"
- }
- ],
- "title": "Body",
- },
- }
- },
- "required": True,
- },
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {"application/json": {"schema": {}}},
- },
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- },
- },
- },
- }
- },
- "/upload-multiple/": {
- "post": {
- "summary": "Upload Multiple Files",
- "operationId": "upload_multiple_files_upload_multiple__post",
- "requestBody": {
- "content": {
- "multipart/form-data": {
- "schema": {
- "allOf": [
- {
- "$ref": "#/components/schemas/Body_upload_multiple_files_upload_multiple__post"
- }
- ],
- "title": "Body",
- }
- }
- },
- "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": {
- "Body_create_item_embed_items_embed__post": {
- "properties": {
- "item": {
- "allOf": [{"$ref": "#/components/schemas/Item"}],
- "title": "Item",
- }
- },
- "type": "object",
- "required": ["item"],
- "title": "Body_create_item_embed_items_embed__post",
- },
- "Body_submit_form_form_data__post": {
- "properties": {
- "username": {
- "type": "string",
- "maxLength": 50,
- "minLength": 3,
- "title": "Username",
- },
- "password": {
- "type": "string",
- "minLength": 8,
- "title": "Password",
- },
- "email": {"type": "string", "title": "Email"},
- },
- "type": "object",
- "required": ["username", "password"],
- "title": "Body_submit_form_form_data__post",
- },
- "Body_update_item_items__item_id__put": {
- "properties": {
- "item": {
- "allOf": [{"$ref": "#/components/schemas/Item"}],
- "title": "Item",
- },
- "importance": {
- "type": "integer",
- "maximum": 10.0,
- "exclusiveMinimum": 0.0,
- "title": "Importance",
- },
- },
- "type": "object",
- "required": ["item", "importance"],
- "title": "Body_update_item_items__item_id__put",
- },
- "Body_upload_file_upload__post": {
- "properties": {
- "file": {
- "type": "string",
- "format": "binary",
- "title": "File",
- },
- "description": {"type": "string", "title": "Description"},
- },
- "type": "object",
- "required": ["file"],
- "title": "Body_upload_file_upload__post",
- },
- "Body_upload_multiple_files_upload_multiple__post": {
- "properties": {
- "files": {
- "items": {"type": "string", "format": "binary"},
- "type": "array",
- "title": "Files",
- },
- "note": {"type": "string", "title": "Note", "default": ""},
- },
- "type": "object",
- "required": ["files"],
- "title": "Body_upload_multiple_files_upload_multiple__post",
- },
- "HTTPValidationError": {
- "properties": {
- "detail": {
- "items": {
- "$ref": "#/components/schemas/ValidationError"
- },
- "type": "array",
- "title": "Detail",
- }
- },
- "type": "object",
- "title": "HTTPValidationError",
- },
- "Item": {
- "properties": {
- "name": {"type": "string", "title": "Name"},
- "price": {"type": "number", "title": "Price"},
- "description": {"type": "string", "title": "Description"},
- },
- "type": "object",
- "required": ["name", "price"],
- "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",
- },
- }
- },
- }
- )
-import warnings
from datetime import datetime, timezone
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
-from .utils import needs_pydanticv1
-
def test_pydanticv2():
from pydantic import field_serializer
with client:
response = client.get("/model")
assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"}
-
-
-# TODO: remove when deprecating Pydantic v1
-@needs_pydanticv1
-def test_pydanticv1():
- from pydantic import v1
-
- class ModelWithDatetimeField(v1.BaseModel):
- dt_field: datetime
-
- class Config:
- json_encoders = {
- datetime: lambda dt: dt.replace(
- microsecond=0, tzinfo=timezone.utc
- ).isoformat()
- }
-
- app = FastAPI()
- model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
-
- with warnings.catch_warnings(record=True):
- warnings.simplefilter("always")
-
- @app.get("/model", response_model=ModelWithDatetimeField)
- def get_model():
- return model
-
- client = TestClient(app)
- with client:
- response = client.get("/model")
- assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"}
+++ /dev/null
-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"},
- }
+++ /dev/null
-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"},
- },
- },
- }
- },
- }
- )
-import warnings
-
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
-from .utils import needs_pydanticv1
-
-@pytest.fixture(
- name="client",
- params=[
- pytest.param("pydantic-v1", marks=needs_pydanticv1),
- "pydantic-v2",
- ],
-)
-def client_fixture(request: pytest.FixtureRequest) -> TestClient:
- if request.param == "pydantic-v1":
- from pydantic.v1 import BaseModel
- else:
- from pydantic import BaseModel
+@pytest.fixture(name="client")
+def client_fixture() -> TestClient:
+ from pydantic import BaseModel
class Address(BaseModel):
"""
app = FastAPI()
- if request.param == "pydantic-v1":
- with warnings.catch_warnings(record=True):
- warnings.simplefilter("always")
-
- @app.get("/facilities/{facility_id}")
- def get_facility(facility_id: str) -> Facility:
- return Facility(
- id=facility_id,
- address=Address(
- line_1="123 Main St", city="Anytown", state_province="CA"
- ),
- )
- else:
-
- @app.get("/facilities/{facility_id}")
- def get_facility(facility_id: str) -> Facility:
- return Facility(
- id=facility_id,
- address=Address(
- line_1="123 Main St", city="Anytown", state_province="CA"
- ),
- )
+ @app.get("/facilities/{facility_id}")
+ def get_facility(facility_id: str) -> Facility:
+ return Facility(
+ id=facility_id,
+ address=Address(line_1="123 Main St", city="Anytown", state_province="CA"),
+ )
client = TestClient(app)
return client
from fastapi.testclient import TestClient
from pydantic import BaseModel
-from .utils import needs_pydanticv1
-
class MyUuid:
def __init__(self, uuid_string: str):
assert response_pydantic.json() == {
"a_uuid": "b8799909-f914-42de-91bc-95c819218d01"
}
-
-
-# TODO: remove when deprecating Pydantic v1
-@needs_pydanticv1
-def test_pydanticv1():
- from pydantic import v1
-
- app = FastAPI()
-
- @app.get("/fast_uuid")
- def return_fast_uuid():
- asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
- assert isinstance(asyncpg_uuid, uuid.UUID)
- assert type(asyncpg_uuid) is not uuid.UUID
- with pytest.raises(TypeError):
- vars(asyncpg_uuid)
- return {"fast_uuid": asyncpg_uuid}
-
- class SomeCustomClass(v1.BaseModel):
- class Config:
- arbitrary_types_allowed = True
- json_encoders = {uuid.UUID: str}
-
- a_uuid: MyUuid
-
- @app.get("/get_custom_class")
- def return_some_user():
- # Test that the fix also works for custom pydantic classes
- return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
-
- client = TestClient(app)
-
- with client:
- response_simple = client.get("/fast_uuid")
- response_pydantic = client.get("/get_custom_class")
-
- assert response_simple.json() == {
- "fast_uuid": "a10ff360-3b1e-4984-a26f-d3ab460bdb51"
- }
-
- assert response_pydantic.json() == {
- "a_uuid": "b8799909-f914-42de-91bc-95c819218d01"
- }
+import warnings
from collections import deque
from dataclasses import dataclass
from datetime import datetime, timezone
from enum import Enum
from math import isinf, isnan
from pathlib import PurePath, PurePosixPath, PureWindowsPath
-from typing import Optional
+from typing import Optional, TypedDict
import pytest
from fastapi._compat import Undefined
from fastapi.encoders import jsonable_encoder
+from fastapi.exceptions import PydanticV1NotSupportedError
from pydantic import BaseModel, Field, ValidationError
-from .utils import needs_pydanticv1
-
class Person:
def __init__(self, name: str):
assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
-# TODO: remove when deprecating Pydantic v1
-@needs_pydanticv1
-def test_encode_custom_json_encoders_model_pydanticv1():
- from pydantic import v1
-
- class ModelWithCustomEncoder(v1.BaseModel):
- dt_field: datetime
+def test_json_encoder_error_with_pydanticv1():
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", UserWarning)
+ from pydantic import v1
- class Config:
- json_encoders = {
- datetime: lambda dt: dt.replace(
- microsecond=0, tzinfo=timezone.utc
- ).isoformat()
- }
-
- class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder):
- class Config:
- pass
+ class ModelV1(v1.BaseModel):
+ name: str
- model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8))
- assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
- subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8))
- assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
+ data = ModelV1(name="test")
+ with pytest.raises(PydanticV1NotSupportedError):
+ jsonable_encoder(data)
def test_encode_model_with_config():
}
-@needs_pydanticv1
def test_custom_encoders():
- from pydantic import v1
-
class safe_datetime(datetime):
pass
- class MyModel(v1.BaseModel):
+ class MyDict(TypedDict):
dt_field: safe_datetime
- instance = MyModel(dt_field=safe_datetime.now())
+ instance = MyDict(dt_field=safe_datetime.now())
encoded_instance = jsonable_encoder(
instance, custom_encoder={safe_datetime: lambda o: o.strftime("%H:%M:%S")}
)
- assert encoded_instance["dt_field"] == instance.dt_field.strftime("%H:%M:%S")
+ assert encoded_instance["dt_field"] == instance["dt_field"].strftime("%H:%M:%S")
+
+ encoded_instance = jsonable_encoder(
+ instance, custom_encoder={datetime: lambda o: o.strftime("%H:%M:%S")}
+ )
+ assert encoded_instance["dt_field"] == instance["dt_field"].strftime("%H:%M:%S")
encoded_instance2 = jsonable_encoder(instance)
- assert encoded_instance2["dt_field"] == instance.dt_field.isoformat()
+ assert encoded_instance2["dt_field"] == instance["dt_field"].isoformat()
def test_custom_enum_encoders():
assert jsonable_encoder({"path": test_path}) == {"path": str(test_path)}
-@needs_pydanticv1
-def test_encode_root():
- from pydantic import v1
-
- class ModelWithRoot(v1.BaseModel):
- __root__: str
-
- model = ModelWithRoot(__root__="Foo")
- assert jsonable_encoder(model) == "Foo"
-
-
def test_decimal_encoder_float():
data = {"value": Decimal(1.23)}
assert jsonable_encoder(data) == {"value": 1.23}
+++ /dev/null
-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}
--- /dev/null
+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
+++ /dev/null
-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",
- },
- }
- },
- }
- )
+++ /dev/null
-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",
- },
- }
- },
- }
- )
+++ /dev/null
-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/item")
- def handle_v1_item_to_v2(data: Item) -> NewItem:
- 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=NewItem)
- def handle_v1_item_to_v2_filter(data: Item) -> Any:
- 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) -> Item:
- 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=Item)
- def handle_v2_item_to_v1_filter(data: NewItem) -> Any:
- 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
-
- @app.post("/v1-to-v2/item-to-list")
- def handle_v1_item_to_v2_list(data: Item) -> list[NewItem]:
- converted = 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],
- )
- return [converted, converted]
-
- @app.post("/v1-to-v2/list-to-list")
- def handle_v1_list_to_v2_list(data: list[Item]) -> list[NewItem]:
- result = []
- for item in data:
- result.append(
- NewItem(
- new_title=item.title,
- new_size=item.size,
- new_description=item.description,
- new_sub=NewSubItem(new_sub_name=item.sub.name),
- new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi],
- )
- )
- return result
-
- @app.post("/v1-to-v2/list-to-list-filter", response_model=list[NewItem])
- def handle_v1_list_to_v2_list_filter(data: list[Item]) -> Any:
- result = []
- for item in data:
- converted = {
- "new_title": item.title,
- "new_size": item.size,
- "new_description": item.description,
- "new_sub": {
- "new_sub_name": item.sub.name,
- "new_sub_secret": "sub_hidden",
- },
- "new_multi": [
- {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"}
- for s in item.multi
- ],
- "secret": "hidden_v2_to_v1",
- }
- result.append(converted)
- return result
-
- @app.post("/v1-to-v2/list-to-item")
- def handle_v1_list_to_v2_item(data: list[Item]) -> NewItem:
- if data:
- item = data[0]
- return NewItem(
- new_title=item.title,
- new_size=item.size,
- new_description=item.description,
- new_sub=NewSubItem(new_sub_name=item.sub.name),
- new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi],
- )
- return NewItem(new_title="", new_size=0, new_sub=NewSubItem(new_sub_name=""))
-
- @app.post("/v2-to-v1/item-to-list")
- def handle_v2_item_to_v1_list(data: NewItem) -> list[Item]:
- converted = 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],
- )
- return [converted, converted]
-
- @app.post("/v2-to-v1/list-to-list")
- def handle_v2_list_to_v1_list(data: list[NewItem]) -> list[Item]:
- result = []
- for item in data:
- result.append(
- Item(
- title=item.new_title,
- size=item.new_size,
- description=item.new_description,
- sub=SubItem(name=item.new_sub.new_sub_name),
- multi=[SubItem(name=s.new_sub_name) for s in item.new_multi],
- )
- )
- return result
-
- @app.post("/v2-to-v1/list-to-list-filter", response_model=list[Item])
- def handle_v2_list_to_v1_list_filter(data: list[NewItem]) -> Any:
- result = []
- for item in data:
- converted = {
- "title": item.new_title,
- "size": item.new_size,
- "description": item.new_description,
- "sub": {
- "name": item.new_sub.new_sub_name,
- "sub_secret": "sub_hidden",
- },
- "multi": [
- {"name": s.new_sub_name, "sub_secret": "sub_hidden"}
- for s in item.new_multi
- ],
- "secret": "hidden_v2_to_v1",
- }
- result.append(converted)
- return result
-
- @app.post("/v2-to-v1/list-to-item")
- def handle_v2_list_to_v1_item(data: list[NewItem]) -> Item:
- if data:
- item = data[0]
- return Item(
- title=item.new_title,
- size=item.new_size,
- description=item.new_description,
- sub=SubItem(name=item.new_sub.new_sub_name),
- multi=[SubItem(name=s.new_sub_name) for s in item.new_multi],
- )
- return Item(title="", size=0, sub=SubItem(name=""))
-
-
-client = TestClient(app)
-
-
-def test_v1_to_v2_item():
- response = client.post(
- "/v1-to-v2/item",
- 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_minimal():
- response = client.post(
- "/v1-to-v2/item",
- 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():
- 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 == snapshot(
- {
- "new_title": "Filtered Item",
- "new_size": 50,
- "new_description": None,
- "new_sub": {"new_sub_name": "Sub"},
- "new_multi": [{"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_v2_to_v1_item():
- 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_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():
- 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 == snapshot(
- {
- "title": "Filtered New",
- "size": 75,
- "description": None,
- "sub": {"name": "NewSub"},
- "multi": [],
- }
- )
- # Verify secret fields are filtered out
- assert "secret" not in result
- assert "sub_secret" not in result["sub"]
-
-
-def test_v1_item_to_v2_list():
- response = client.post(
- "/v1-to-v2/item-to-list",
- json={
- "title": "Single to List",
- "size": 150,
- "description": "Convert to list",
- "sub": {"name": "Sub1"},
- "multi": [],
- },
- )
- assert response.status_code == 200, response.text
- result = response.json()
- assert result == [
- {
- "new_title": "Single to List",
- "new_size": 150,
- "new_description": "Convert to list",
- "new_sub": {"new_sub_name": "Sub1"},
- "new_multi": [],
- },
- {
- "new_title": "Single to List",
- "new_size": 150,
- "new_description": "Convert to list",
- "new_sub": {"new_sub_name": "Sub1"},
- "new_multi": [],
- },
- ]
-
-
-def test_v1_list_to_v2_list():
- response = client.post(
- "/v1-to-v2/list-to-list",
- json=[
- {"title": "Item1", "size": 10, "sub": {"name": "Sub1"}},
- {
- "title": "Item2",
- "size": 20,
- "description": "Second item",
- "sub": {"name": "Sub2"},
- "multi": [{"name": "M1"}, {"name": "M2"}],
- },
- {"title": "Item3", "size": 30, "sub": {"name": "Sub3"}},
- ],
- )
- assert response.status_code == 200, response.text
- assert response.json() == [
- {
- "new_title": "Item1",
- "new_size": 10,
- "new_description": None,
- "new_sub": {"new_sub_name": "Sub1"},
- "new_multi": [],
- },
- {
- "new_title": "Item2",
- "new_size": 20,
- "new_description": "Second item",
- "new_sub": {"new_sub_name": "Sub2"},
- "new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}],
- },
- {
- "new_title": "Item3",
- "new_size": 30,
- "new_description": None,
- "new_sub": {"new_sub_name": "Sub3"},
- "new_multi": [],
- },
- ]
-
-
-def test_v1_list_to_v2_list_filter():
- response = client.post(
- "/v1-to-v2/list-to-list-filter",
- json=[{"title": "FilterMe", "size": 30, "sub": {"name": "SubF"}}],
- )
- assert response.status_code == 200, response.text
- result = response.json()
- assert result == snapshot(
- [
- {
- "new_title": "FilterMe",
- "new_size": 30,
- "new_description": None,
- "new_sub": {"new_sub_name": "SubF"},
- "new_multi": [],
- }
- ]
- )
- # Verify secret fields are filtered out
- assert "secret" not in result[0]
- assert "new_sub_secret" not in result[0]["new_sub"]
-
-
-def test_v1_list_to_v2_item():
- response = client.post(
- "/v1-to-v2/list-to-item",
- json=[
- {"title": "First", "size": 100, "sub": {"name": "FirstSub"}},
- {"title": "Second", "size": 200, "sub": {"name": "SecondSub"}},
- ],
- )
- assert response.status_code == 200, response.text
- assert response.json() == {
- "new_title": "First",
- "new_size": 100,
- "new_description": None,
- "new_sub": {"new_sub_name": "FirstSub"},
- "new_multi": [],
- }
-
-
-def test_v1_list_to_v2_item_empty():
- response = client.post("/v1-to-v2/list-to-item", json=[])
- assert response.status_code == 200, response.text
- assert response.json() == {
- "new_title": "",
- "new_size": 0,
- "new_description": None,
- "new_sub": {"new_sub_name": ""},
- "new_multi": [],
- }
-
-
-def test_v2_item_to_v1_list():
- response = client.post(
- "/v2-to-v1/item-to-list",
- json={
- "new_title": "Single New",
- "new_size": 250,
- "new_description": "New to list",
- "new_sub": {"new_sub_name": "NewSub"},
- "new_multi": [],
- },
- )
- assert response.status_code == 200, response.text
- assert response.json() == [
- {
- "title": "Single New",
- "size": 250,
- "description": "New to list",
- "sub": {"name": "NewSub"},
- "multi": [],
- },
- {
- "title": "Single New",
- "size": 250,
- "description": "New to list",
- "sub": {"name": "NewSub"},
- "multi": [],
- },
- ]
-
-
-def test_v2_list_to_v1_list():
- response = client.post(
- "/v2-to-v1/list-to-list",
- json=[
- {"new_title": "New1", "new_size": 15, "new_sub": {"new_sub_name": "NS1"}},
- {
- "new_title": "New2",
- "new_size": 25,
- "new_description": "Second new",
- "new_sub": {"new_sub_name": "NS2"},
- "new_multi": [{"new_sub_name": "NM1"}],
- },
- ],
- )
- assert response.status_code == 200, response.text
- assert response.json() == [
- {
- "title": "New1",
- "size": 15,
- "description": None,
- "sub": {"name": "NS1"},
- "multi": [],
- },
- {
- "title": "New2",
- "size": 25,
- "description": "Second new",
- "sub": {"name": "NS2"},
- "multi": [{"name": "NM1"}],
- },
- ]
-
-
-def test_v2_list_to_v1_list_filter():
- response = client.post(
- "/v2-to-v1/list-to-list-filter",
- json=[
- {
- "new_title": "FilterNew",
- "new_size": 35,
- "new_sub": {"new_sub_name": "NSF"},
- }
- ],
- )
- assert response.status_code == 200, response.text
- result = response.json()
- assert result == snapshot(
- [
- {
- "title": "FilterNew",
- "size": 35,
- "description": None,
- "sub": {"name": "NSF"},
- "multi": [],
- }
- ]
- )
- # Verify secret fields are filtered out
- assert "secret" not in result[0]
- assert "sub_secret" not in result[0]["sub"]
-
-
-def test_v2_list_to_v1_item():
- response = client.post(
- "/v2-to-v1/list-to-item",
- json=[
- {
- "new_title": "FirstNew",
- "new_size": 300,
- "new_sub": {"new_sub_name": "FNS"},
- },
- {
- "new_title": "SecondNew",
- "new_size": 400,
- "new_sub": {"new_sub_name": "SNS"},
- },
- ],
- )
- assert response.status_code == 200, response.text
- assert response.json() == {
- "title": "FirstNew",
- "size": 300,
- "description": None,
- "sub": {"name": "FNS"},
- "multi": [],
- }
-
-
-def test_v2_list_to_v1_item_empty():
- response = client.post("/v2-to-v1/list-to-item", json=[])
- assert response.status_code == 200, response.text
- assert response.json() == {
- "title": "",
- "size": 0,
- "description": None,
- "sub": {"name": ""},
- "multi": [],
- }
-
-
-def test_v1_to_v2_validation_error():
- response = client.post("/v1-to-v2/item", 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/item",
- json={"title": "Bad sub", "size": 100, "sub": {"wrong_field": "value"}},
- )
- 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_v1_to_v2_type_validation_error():
- response = client.post(
- "/v1-to-v2/item",
- json={"title": "Bad type", "size": "not_a_number", "sub": {"name": "Sub"}},
- )
- 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_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_v1_list_validation_error():
- response = client.post(
- "/v1-to-v2/list-to-list",
- json=[
- {"title": "Valid", "size": 10, "sub": {"name": "S"}},
- {"title": "Invalid"},
- ],
- )
- assert response.status_code == 422, response.text
- assert response.json() == snapshot(
- {
- "detail": [
- {
- "loc": ["body", 1, "size"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", 1, "sub"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
- }
- )
-
-
-def test_v2_list_validation_error():
- response = client.post(
- "/v2-to-v1/list-to-list",
- json=[
- {"new_title": "Valid", "new_size": 10, "new_sub": {"new_sub_name": "NS"}},
- {"new_title": "Invalid"},
- ],
- )
- assert response.status_code == 422, response.text
- assert response.json() == snapshot(
- {
- "detail": [
- {
- "type": "missing",
- "loc": ["body", 1, "new_size"],
- "msg": "Field required",
- "input": {"new_title": "Invalid"},
- },
- {
- "type": "missing",
- "loc": ["body", 1, "new_sub"],
- "msg": "Field required",
- "input": {"new_title": "Invalid"},
- },
- ]
- }
- )
-
-
-def test_invalid_list_structure_v1():
- response = client.post(
- "/v1-to-v2/list-to-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_invalid_list_structure_v2():
- response = client.post(
- "/v2-to-v1/list-to-list",
- json={
- "new_title": "Not a list",
- "new_size": 100,
- "new_sub": {"new_sub_name": "Sub"},
- },
- )
- assert response.status_code == 422, response.text
- assert response.json() == snapshot(
- {
- "detail": [
- {
- "type": "list_type",
- "loc": ["body"],
- "msg": "Input should be a valid list",
- "input": {
- "new_title": "Not a list",
- "new_size": 100,
- "new_sub": {"new_sub_name": "Sub"},
- },
- }
- ]
- }
- )
-
-
-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/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/Item"}
- ],
- "title": "Data",
- }
- }
- },
- "required": True,
- },
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/NewItem"
- }
- }
- },
- },
- "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": {
- "$ref": "#/components/schemas/NewItem"
- }
- }
- },
- },
- "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"
- }
- }
- },
- },
- },
- }
- },
- "/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/Item"}
- ],
- "title": "Data",
- }
- }
- },
- "required": True,
- },
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {
- "application/json": {
- "schema": {
- "items": {
- "$ref": "#/components/schemas/NewItem"
- },
- "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/Item"},
- "type": "array",
- "title": "Data",
- }
- }
- },
- "required": True,
- },
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {
- "application/json": {
- "schema": {
- "items": {
- "$ref": "#/components/schemas/NewItem"
- },
- "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-list-filter": {
- "post": {
- "summary": "Handle V1 List To V2 List Filter",
- "operationId": "handle_v1_list_to_v2_list_filter_v1_to_v2_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/NewItem"
- },
- "type": "array",
- "title": "Response Handle V1 List To V2 List Filter V1 To V2 List To List Filter 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/Item"},
- "type": "array",
- "title": "Data",
- }
- }
- },
- "required": True,
- },
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/NewItem"
- }
- }
- },
- },
- "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/NewItem"}
- }
- },
- "required": True,
- },
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {
- "application/json": {
- "schema": {
- "items": {
- "$ref": "#/components/schemas/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/NewItem"
- },
- "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 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-list-filter": {
- "post": {
- "summary": "Handle V2 List To V1 List Filter",
- "operationId": "handle_v2_list_to_v1_list_filter_v2_to_v1_list_to_list_filter_post",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "items": {
- "$ref": "#/components/schemas/NewItem"
- },
- "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 V2 List To V1 List Filter V2 To V1 List To List Filter 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/NewItem"
- },
- "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"
- }
- }
- },
- },
- },
- }
- },
- },
- "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",
- },
- }
- },
- }
- )
+++ /dev/null
-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),
- ]
+++ /dev/null
-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
+++ /dev/null
-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
+++ /dev/null
-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
+++ /dev/null
-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",
- },
- },
- },
- }
- )
+++ /dev/null
-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",
- },
- }
- },
- }
- )
-import warnings
from typing import Any
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict
-from .utils import needs_pydanticv1
-
def test_read_with_orm_mode() -> None:
class PersonBase(BaseModel):
assert data["name"] == person_data["name"]
assert data["lastname"] == person_data["lastname"]
assert data["full_name"] == person_data["name"] + " " + person_data["lastname"]
-
-
-@needs_pydanticv1
-def test_read_with_orm_mode_pv1() -> None:
- from pydantic import v1
-
- class PersonBase(v1.BaseModel):
- name: str
- lastname: str
-
- class Person(PersonBase):
- @property
- def full_name(self) -> str:
- return f"{self.name} {self.lastname}"
-
- class Config:
- orm_mode = True
- read_with_orm_mode = True
-
- class PersonCreate(PersonBase):
- pass
-
- class PersonRead(PersonBase):
- full_name: str
-
- class Config:
- orm_mode = True
-
- app = FastAPI()
-
- with warnings.catch_warnings(record=True):
- warnings.simplefilter("always")
-
- @app.post("/people/", response_model=PersonRead)
- def create_person(person: PersonCreate) -> Any:
- db_person = Person.from_orm(person)
- return db_person
-
- client = TestClient(app)
-
- person_data = {"name": "Dive", "lastname": "Wilson"}
- response = client.post("/people/", json=person_data)
- data = response.json()
- assert response.status_code == 200, response.text
- assert data["name"] == person_data["name"]
- assert data["lastname"] == person_data["lastname"]
- assert data["full_name"] == person_data["name"] + " " + person_data["lastname"]
-import warnings
from typing import Union
import pytest
from fastapi.testclient import TestClient
from pydantic import BaseModel
-from tests.utils import needs_pydanticv1
-
class BaseUser(BaseModel):
name: str
assert "parameter response_model=None" in e.value.args[0]
-# TODO: remove when dropping Pydantic v1 support
-@needs_pydanticv1
-def test_invalid_response_model_field_pv1():
- from fastapi._compat import v1
-
- app = FastAPI()
-
- class Model(v1.BaseModel):
- foo: str
-
- with warnings.catch_warnings(record=True):
- warnings.simplefilter("always")
-
- with pytest.raises(FastAPIError) as e:
-
- @app.get("/")
- def read_root() -> Union[Response, Model, None]:
- return Response(content="Foo") # pragma: no cover
-
- assert "valid Pydantic field type" in e.value.args[0]
- assert "parameter response_model=None" in e.value.args[0]
-
-
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
+++ /dev/null
-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": {}}},
- }
- },
- }
- }
- },
- }
+++ /dev/null
-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}
+++ /dev/null
-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",
- },
- }
- },
- }
- )
+++ /dev/null
-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",
- },
- }
- },
- }
- )
+++ /dev/null
-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",
- },
- }
- },
- }
- )
+++ /dev/null
-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",
- },
- ]
- }
+++ /dev/null
-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",
- },
- }
- },
- }
- )
from fastapi.testclient import TestClient
from pytest import MonkeyPatch
-from ...utils import needs_pydanticv1
-
@pytest.fixture(
name="mod_path",
assert settings.items_per_user == 50
-@needs_pydanticv1
-def test_settings_pv1(mod_path: str, monkeypatch: MonkeyPatch):
- monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com")
- config_mod = importlib.import_module(f"{mod_path}.config_pv1")
- settings = config_mod.Settings()
- assert settings.app_name == "Awesome API"
- assert settings.admin_email == "admin@example.com"
- assert settings.items_per_user == 50
-
-
def test_endpoint(main_mod: ModuleType, monkeypatch: MonkeyPatch):
monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com")
client = TestClient(main_mod.app)
from fastapi.testclient import TestClient
from pytest import MonkeyPatch
-from ...utils import needs_pydanticv1
-
-@pytest.fixture(
- name="app",
- params=[
- pytest.param("tutorial001_py39"),
- pytest.param("tutorial001_pv1_py39", marks=needs_pydanticv1),
- ],
-)
+@pytest.fixture(name="app", params=[pytest.param("tutorial001_py39")])
def get_app(request: pytest.FixtureRequest, monkeypatch: MonkeyPatch):
monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com")
mod = importlib.import_module(f"docs_src.settings.{request.param}")
sys.version_info >= (3, 14), reason="requires python3.13-"
)
-needs_pydanticv1 = needs_py_lt_314
-
def skip_module_if_py_gte_314():
"""Skip entire module on Python 3.14+ at import time."""