From: Sebastián Ramírez Date: Sat, 27 Dec 2025 12:54:56 +0000 (-0800) Subject: ➖ Drop support for `pydantic.v1` (#14609) X-Git-Tag: 0.128.0~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e3006305518a56ea35f62a31748ad26fe4356fcc;p=thirdparty%2Ffastapi%2Ffastapi.git ➖ Drop support for `pydantic.v1` (#14609) --- diff --git a/docs_src/request_form_models/tutorial002_pv1_an_py39.py b/docs_src/request_form_models/tutorial002_pv1_an_py39.py deleted file mode 100644 index 392e6873cb..0000000000 --- a/docs_src/request_form_models/tutorial002_pv1_an_py39.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI -from fastapi.temp_pydantic_v1_params import Form -from pydantic.v1 import BaseModel - -app = FastAPI() - - -class FormData(BaseModel): - username: str - password: str - - class Config: - extra = "forbid" - - -@app.post("/login/") -async def login(data: Annotated[FormData, Form()]): - return data diff --git a/docs_src/request_form_models/tutorial002_pv1_py39.py b/docs_src/request_form_models/tutorial002_pv1_py39.py deleted file mode 100644 index da160b3a54..0000000000 --- a/docs_src/request_form_models/tutorial002_pv1_py39.py +++ /dev/null @@ -1,18 +0,0 @@ -from fastapi import FastAPI -from fastapi.temp_pydantic_v1_params import Form -from pydantic.v1 import BaseModel - -app = FastAPI() - - -class FormData(BaseModel): - username: str - password: str - - class Config: - extra = "forbid" - - -@app.post("/login/") -async def login(data: FormData = Form()): - return data diff --git a/fastapi/_compat/__init__.py b/fastapi/_compat/__init__.py index fd1df8c6a7..3dfaf9b712 100644 --- a/fastapi/_compat/__init__.py +++ b/fastapi/_compat/__init__.py @@ -1,43 +1,9 @@ -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, ) @@ -47,3 +13,29 @@ from .shared import ( from .shared import lenient_issubclass as lenient_issubclass from .shared import sequence_types as sequence_types from .shared import value_is_sequence as value_is_sequence +from .v2 import 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, +) diff --git a/fastapi/_compat/main.py b/fastapi/_compat/main.py deleted file mode 100644 index 95053a2374..0000000000 --- a/fastapi/_compat/main.py +++ /dev/null @@ -1,264 +0,0 @@ -import sys -from collections.abc import Sequence -from functools import lru_cache -from typing import ( - Any, -) - -from fastapi._compat import may_v1 -from fastapi._compat.shared import lenient_issubclass -from fastapi.types import ModelNameMap -from pydantic import BaseModel -from typing_extensions import Literal - -from . import v2 -from .model_field import ModelField -from .v2 import BaseConfig as BaseConfig -from .v2 import FieldInfo as FieldInfo -from .v2 import PydanticSchemaGenerationError as PydanticSchemaGenerationError -from .v2 import RequiredParam as RequiredParam -from .v2 import Undefined as Undefined -from .v2 import UndefinedType as UndefinedType -from .v2 import Url as Url -from .v2 import Validator as Validator -from .v2 import evaluate_forwardref as evaluate_forwardref -from .v2 import get_missing_field_error as get_missing_field_error -from .v2 import ( - with_info_plain_validator_function as with_info_plain_validator_function, -) - - -@lru_cache -def get_cached_model_fields(model: type[BaseModel]) -> list[ModelField]: - if lenient_issubclass(model, may_v1.BaseModel): - from fastapi._compat import v1 - - return v1.get_model_fields(model) # type: ignore[arg-type,return-value] - else: - from . import v2 - - return v2.get_model_fields(model) # type: ignore[return-value] - - -def _is_undefined(value: object) -> bool: - if isinstance(value, may_v1.UndefinedType): - return True - - return isinstance(value, v2.UndefinedType) - - -def _get_model_config(model: BaseModel) -> Any: - if isinstance(model, may_v1.BaseModel): - from fastapi._compat import v1 - - return v1._get_model_config(model) - - return v2._get_model_config(model) - - -def _model_dump( - model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any -) -> Any: - if isinstance(model, may_v1.BaseModel): - from fastapi._compat import v1 - - return v1._model_dump(model, mode=mode, **kwargs) - - return v2._model_dump(model, mode=mode, **kwargs) - - -def _is_error_wrapper(exc: Exception) -> bool: - if isinstance(exc, may_v1.ErrorWrapper): - return True - - return isinstance(exc, v2.ErrorWrapper) - - -def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: - if isinstance(field_info, may_v1.FieldInfo): - from fastapi._compat import v1 - - return v1.copy_field_info(field_info=field_info, annotation=annotation) - - return v2.copy_field_info(field_info=field_info, annotation=annotation) - - -def create_body_model( - *, fields: Sequence[ModelField], model_name: str -) -> type[BaseModel]: - if fields and isinstance(fields[0], may_v1.ModelField): - from fastapi._compat import v1 - - return v1.create_body_model(fields=fields, model_name=model_name) - - return v2.create_body_model(fields=fields, model_name=model_name) # type: ignore[arg-type] - - -def get_annotation_from_field_info( - annotation: Any, field_info: FieldInfo, field_name: str -) -> Any: - if isinstance(field_info, may_v1.FieldInfo): - from fastapi._compat import v1 - - return v1.get_annotation_from_field_info( - annotation=annotation, field_info=field_info, field_name=field_name - ) - - return v2.get_annotation_from_field_info( - annotation=annotation, field_info=field_info, field_name=field_name - ) - - -def is_bytes_field(field: ModelField) -> bool: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.is_bytes_field(field) - - return v2.is_bytes_field(field) # type: ignore[arg-type] - - -def is_bytes_sequence_field(field: ModelField) -> bool: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.is_bytes_sequence_field(field) - - return v2.is_bytes_sequence_field(field) # type: ignore[arg-type] - - -def is_scalar_field(field: ModelField) -> bool: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.is_scalar_field(field) - - return v2.is_scalar_field(field) # type: ignore[arg-type] - - -def is_scalar_sequence_field(field: ModelField) -> bool: - return v2.is_scalar_sequence_field(field) # type: ignore[arg-type] - - -def is_sequence_field(field: ModelField) -> bool: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.is_sequence_field(field) - - return v2.is_sequence_field(field) # type: ignore[arg-type] - - -def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.serialize_sequence_value(field=field, value=value) - - return v2.serialize_sequence_value(field=field, value=value) # type: ignore[arg-type] - - -def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap: - v1_model_fields = [ - field for field in fields if isinstance(field, may_v1.ModelField) - ] - if v1_model_fields: - from fastapi._compat import v1 - - v1_flat_models = v1.get_flat_models_from_fields( - v1_model_fields, # type: ignore[arg-type] - known_models=set(), - ) - all_flat_models = v1_flat_models - else: - all_flat_models = set() - - v2_model_fields = [field for field in fields if isinstance(field, v2.ModelField)] - v2_flat_models = v2.get_flat_models_from_fields(v2_model_fields, known_models=set()) - all_flat_models = all_flat_models.union(v2_flat_models) # type: ignore[arg-type] - - model_name_map = v2.get_model_name_map(all_flat_models) # type: ignore[arg-type] - return model_name_map - - -def get_definitions( - *, - fields: list[ModelField], - model_name_map: ModelNameMap, - separate_input_output_schemas: bool = True, -) -> tuple[ - dict[ - tuple[ModelField, Literal["validation", "serialization"]], - may_v1.JsonSchemaValue, - ], - dict[str, dict[str, Any]], -]: - if sys.version_info < (3, 14): - v1_fields = [field for field in fields if isinstance(field, may_v1.ModelField)] - v1_field_maps, v1_definitions = may_v1.get_definitions( - fields=v1_fields, # type: ignore[arg-type] - model_name_map=model_name_map, - separate_input_output_schemas=separate_input_output_schemas, - ) - - v2_fields = [field for field in fields if isinstance(field, v2.ModelField)] - v2_field_maps, v2_definitions = v2.get_definitions( - fields=v2_fields, - model_name_map=model_name_map, - separate_input_output_schemas=separate_input_output_schemas, - ) - all_definitions = {**v1_definitions, **v2_definitions} - all_field_maps = {**v1_field_maps, **v2_field_maps} # type: ignore[misc] - return all_field_maps, all_definitions - - # Pydantic v1 is not supported since Python 3.14 - else: - v2_fields = [field for field in fields if isinstance(field, v2.ModelField)] - v2_field_maps, v2_definitions = v2.get_definitions( - fields=v2_fields, - model_name_map=model_name_map, - separate_input_output_schemas=separate_input_output_schemas, - ) - return v2_field_maps, v2_definitions - - -def get_schema_from_model_field( - *, - field: ModelField, - model_name_map: ModelNameMap, - field_mapping: dict[ - tuple[ModelField, Literal["validation", "serialization"]], - may_v1.JsonSchemaValue, - ], - separate_input_output_schemas: bool = True, -) -> dict[str, Any]: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.get_schema_from_model_field( - field=field, - model_name_map=model_name_map, - field_mapping=field_mapping, - separate_input_output_schemas=separate_input_output_schemas, - ) - - return v2.get_schema_from_model_field( - field=field, # type: ignore[arg-type] - model_name_map=model_name_map, - field_mapping=field_mapping, # type: ignore[arg-type] - separate_input_output_schemas=separate_input_output_schemas, - ) - - -def _is_model_field(value: Any) -> bool: - if isinstance(value, may_v1.ModelField): - return True - - return isinstance(value, v2.ModelField) - - -def _is_model_class(value: Any) -> bool: - if lenient_issubclass(value, may_v1.BaseModel): - return True - - return lenient_issubclass(value, v2.BaseModel) # type: ignore[attr-defined] diff --git a/fastapi/_compat/may_v1.py b/fastapi/_compat/may_v1.py deleted file mode 100644 index 3ac86aa98b..0000000000 --- a/fastapi/_compat/may_v1.py +++ /dev/null @@ -1,124 +0,0 @@ -import sys -from collections.abc import Sequence -from typing import Any, Literal, Union - -from fastapi.types import ModelNameMap - -if sys.version_info >= (3, 14): - - class AnyUrl: - pass - - class BaseConfig: - pass - - class BaseModel: - pass - - class Color: - pass - - class CoreSchema: - pass - - class ErrorWrapper: - pass - - class FieldInfo: - pass - - class GetJsonSchemaHandler: - pass - - class JsonSchemaValue: - pass - - class ModelField: - pass - - class NameEmail: - pass - - class RequiredParam: - pass - - class SecretBytes: - pass - - class SecretStr: - pass - - class Undefined: - pass - - class UndefinedType: - pass - - class Url: - pass - - from .v2 import ValidationError, create_model - - def get_definitions( - *, - fields: list[ModelField], - model_name_map: ModelNameMap, - separate_input_output_schemas: bool = True, - ) -> tuple[ - dict[ - tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ], - dict[str, dict[str, Any]], - ]: - return {}, {} # pragma: no cover - - -else: - from .v1 import AnyUrl as AnyUrl - from .v1 import BaseConfig as BaseConfig - from .v1 import BaseModel as BaseModel - from .v1 import Color as Color - from .v1 import CoreSchema as CoreSchema - from .v1 import ErrorWrapper as ErrorWrapper - from .v1 import FieldInfo as FieldInfo - from .v1 import GetJsonSchemaHandler as GetJsonSchemaHandler - from .v1 import JsonSchemaValue as JsonSchemaValue - from .v1 import ModelField as ModelField - from .v1 import NameEmail as NameEmail - from .v1 import RequiredParam as RequiredParam - from .v1 import SecretBytes as SecretBytes - from .v1 import SecretStr as SecretStr - from .v1 import Undefined as Undefined - from .v1 import UndefinedType as UndefinedType - from .v1 import Url as Url - from .v1 import ValidationError, create_model - from .v1 import get_definitions as get_definitions - - -RequestErrorModel: type[BaseModel] = create_model("Request") - - -def _normalize_errors(errors: Sequence[Any]) -> list[dict[str, Any]]: - use_errors: list[Any] = [] - for error in errors: - if isinstance(error, ErrorWrapper): - new_errors = ValidationError( - errors=[error], model=RequestErrorModel - ).errors() - use_errors.extend(new_errors) - elif isinstance(error, list): - use_errors.extend(_normalize_errors(error)) - else: - use_errors.append(error) - return use_errors - - -def _regenerate_error_with_loc( - *, errors: Sequence[Any], loc_prefix: tuple[Union[str, int], ...] -) -> list[dict[str, Any]]: - updated_loc_errors: list[Any] = [ - {**err, "loc": loc_prefix + err.get("loc", ())} - for err in _normalize_errors(errors) - ] - - return updated_loc_errors diff --git a/fastapi/_compat/model_field.py b/fastapi/_compat/model_field.py deleted file mode 100644 index 47d05cb946..0000000000 --- a/fastapi/_compat/model_field.py +++ /dev/null @@ -1,50 +0,0 @@ -from typing import ( - Any, - Union, -) - -from fastapi.types import IncEx -from pydantic.fields import FieldInfo -from typing_extensions import Literal, Protocol - - -class ModelField(Protocol): - field_info: "FieldInfo" - name: str - mode: Literal["validation", "serialization"] = "validation" - _version: Literal["v1", "v2"] = "v1" - - @property - def alias(self) -> str: ... - - @property - def required(self) -> bool: ... - - @property - def default(self) -> Any: ... - - @property - def type_(self) -> Any: ... - - def get_default(self) -> Any: ... - - def validate( - self, - value: Any, - values: dict[str, Any] = {}, # noqa: B006 - *, - loc: tuple[Union[int, str], ...] = (), - ) -> tuple[Any, Union[list[dict[str, Any]], None]]: ... - - def serialize( - self, - value: Any, - *, - mode: Literal["json", "python"] = "json", - include: Union[IncEx, None] = None, - exclude: Union[IncEx, None] = None, - by_alias: bool = True, - exclude_unset: bool = False, - exclude_defaults: bool = False, - exclude_none: bool = False, - ) -> Any: ... diff --git a/fastapi/_compat/shared.py b/fastapi/_compat/shared.py index 3a11e88ac9..419b58f7f2 100644 --- a/fastapi/_compat/shared.py +++ b/fastapi/_compat/shared.py @@ -1,6 +1,7 @@ import sys import types import typing +import warnings from collections import deque from collections.abc import Mapping, Sequence from dataclasses import is_dataclass @@ -10,7 +11,6 @@ from typing import ( Union, ) -from fastapi._compat import may_v1 from fastapi.types import UnionType from pydantic import BaseModel from pydantic.version import VERSION as PYDANTIC_VERSION @@ -81,9 +81,7 @@ def value_is_sequence(value: Any) -> bool: def _annotation_is_complex(annotation: Union[type[Any], None]) -> bool: return ( - lenient_issubclass( - annotation, (BaseModel, may_v1.BaseModel, Mapping, UploadFile) - ) + lenient_issubclass(annotation, (BaseModel, Mapping, UploadFile)) or _annotation_is_sequence(annotation) or is_dataclass(annotation) ) @@ -179,13 +177,27 @@ def is_uploadfile_sequence_annotation(annotation: Any) -> bool: ) +def is_pydantic_v1_model_instance(obj: Any) -> bool: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + from pydantic import v1 + return isinstance(obj, v1.BaseModel) + + +def is_pydantic_v1_model_class(cls: Any) -> bool: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + from pydantic import v1 + return lenient_issubclass(cls, v1.BaseModel) + + def annotation_is_pydantic_v1(annotation: Any) -> bool: - if lenient_issubclass(annotation, may_v1.BaseModel): + if is_pydantic_v1_model_class(annotation): return True origin = get_origin(annotation) if origin is Union or origin is UnionType: for arg in get_args(annotation): - if lenient_issubclass(arg, may_v1.BaseModel): + if is_pydantic_v1_model_class(arg): return True if field_annotation_is_sequence(annotation): for sub_annotation in get_args(annotation): diff --git a/fastapi/_compat/v1.py b/fastapi/_compat/v1.py deleted file mode 100644 index b0a9dd35f1..0000000000 --- a/fastapi/_compat/v1.py +++ /dev/null @@ -1,222 +0,0 @@ -from collections.abc import Sequence -from copy import copy -from dataclasses import dataclass, is_dataclass -from enum import Enum -from typing import ( - Any, - Callable, - Union, -) - -from fastapi._compat import shared -from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX -from fastapi.types import ModelNameMap -from pydantic.v1 import BaseConfig as BaseConfig -from pydantic.v1 import BaseModel as BaseModel -from pydantic.v1 import ValidationError as ValidationError -from pydantic.v1 import create_model as create_model -from pydantic.v1.class_validators import Validator as Validator -from pydantic.v1.color import Color as Color -from pydantic.v1.error_wrappers import ErrorWrapper as ErrorWrapper -from pydantic.v1.fields import ( - SHAPE_FROZENSET, - SHAPE_LIST, - SHAPE_SEQUENCE, - SHAPE_SET, - SHAPE_SINGLETON, - SHAPE_TUPLE, - SHAPE_TUPLE_ELLIPSIS, -) -from pydantic.v1.fields import FieldInfo as FieldInfo -from pydantic.v1.fields import ModelField as ModelField -from pydantic.v1.fields import Undefined as Undefined -from pydantic.v1.fields import UndefinedType as UndefinedType -from pydantic.v1.networks import AnyUrl as AnyUrl -from pydantic.v1.networks import NameEmail as NameEmail -from pydantic.v1.schema import TypeModelSet as TypeModelSet -from pydantic.v1.schema import field_schema, model_process_schema -from pydantic.v1.schema import ( - get_annotation_from_field_info as get_annotation_from_field_info, -) -from pydantic.v1.schema import ( - get_flat_models_from_field as get_flat_models_from_field, -) -from pydantic.v1.schema import ( - get_flat_models_from_fields as get_flat_models_from_fields, -) -from pydantic.v1.schema import get_model_name_map as get_model_name_map -from pydantic.v1.types import SecretBytes as SecretBytes -from pydantic.v1.types import SecretStr as SecretStr -from pydantic.v1.typing import evaluate_forwardref as evaluate_forwardref -from pydantic.v1.utils import lenient_issubclass as lenient_issubclass -from pydantic.version import VERSION as PYDANTIC_VERSION -from typing_extensions import Literal - -PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2]) -PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2 -# Keeping old "Required" functionality from Pydantic V1, without -# shadowing typing.Required. -RequiredParam: Any = Ellipsis - - -GetJsonSchemaHandler = Any -JsonSchemaValue = dict[str, Any] -CoreSchema = Any -Url = AnyUrl - -sequence_shapes = { - SHAPE_LIST, - SHAPE_SET, - SHAPE_FROZENSET, - SHAPE_TUPLE, - SHAPE_SEQUENCE, - SHAPE_TUPLE_ELLIPSIS, -} -sequence_shape_to_type = { - SHAPE_LIST: list, - SHAPE_SET: set, - SHAPE_TUPLE: tuple, - SHAPE_SEQUENCE: list, - SHAPE_TUPLE_ELLIPSIS: list, -} - - -@dataclass -class GenerateJsonSchema: - ref_template: str - - -class PydanticSchemaGenerationError(Exception): - pass - - -RequestErrorModel: type[BaseModel] = create_model("Request") - - -def with_info_plain_validator_function( - function: Callable[..., Any], - *, - ref: Union[str, None] = None, - metadata: Any = None, - serialization: Any = None, -) -> Any: - return {} - - -def get_model_definitions( - *, - flat_models: set[Union[type[BaseModel], type[Enum]]], - model_name_map: dict[Union[type[BaseModel], type[Enum]], str], -) -> dict[str, Any]: - definitions: dict[str, dict[str, Any]] = {} - for model in flat_models: - m_schema, m_definitions, m_nested_models = model_process_schema( - model, model_name_map=model_name_map, ref_prefix=REF_PREFIX - ) - definitions.update(m_definitions) - model_name = model_name_map[model] - definitions[model_name] = m_schema - for m_schema in definitions.values(): - if "description" in m_schema: - m_schema["description"] = m_schema["description"].split("\f")[0] - return definitions - - -def is_pv1_scalar_field(field: ModelField) -> bool: - from fastapi import params - - field_info = field.field_info - if not ( - field.shape == SHAPE_SINGLETON - and not lenient_issubclass(field.type_, BaseModel) - and not lenient_issubclass(field.type_, dict) - and not shared.field_annotation_is_sequence(field.type_) - and not is_dataclass(field.type_) - and not isinstance(field_info, params.Body) - ): - return False - if field.sub_fields: - if not all(is_pv1_scalar_field(f) for f in field.sub_fields): - return False - return True - - -def _model_dump( - model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any -) -> Any: - return model.dict(**kwargs) - - -def _get_model_config(model: BaseModel) -> Any: - return model.__config__ - - -def get_schema_from_model_field( - *, - field: ModelField, - model_name_map: ModelNameMap, - field_mapping: dict[ - tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ], - separate_input_output_schemas: bool = True, -) -> dict[str, Any]: - return field_schema( - field, - model_name_map=model_name_map, # type: ignore[arg-type] - ref_prefix=REF_PREFIX, - )[0] - - -# def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: -# models = get_flat_models_from_fields(fields, known_models=set()) -# return get_model_name_map(models) # type: ignore[no-any-return] - - -def get_definitions( - *, - fields: list[ModelField], - model_name_map: ModelNameMap, - separate_input_output_schemas: bool = True, -) -> tuple[ - dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], - dict[str, dict[str, Any]], -]: - models = get_flat_models_from_fields(fields, known_models=set()) - return {}, get_model_definitions(flat_models=models, model_name_map=model_name_map) # type: ignore[arg-type] - - -def is_scalar_field(field: ModelField) -> bool: - return is_pv1_scalar_field(field) - - -def is_sequence_field(field: ModelField) -> bool: - return field.shape in sequence_shapes or shared._annotation_is_sequence(field.type_) - - -def is_bytes_field(field: ModelField) -> bool: - return lenient_issubclass(field.type_, bytes) - - -def is_bytes_sequence_field(field: ModelField) -> bool: - return field.shape in sequence_shapes and lenient_issubclass(field.type_, bytes) - - -def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: - return copy(field_info) - - -def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: - return sequence_shape_to_type[field.shape](value) # type: ignore[no-any-return] - - -def create_body_model( - *, fields: Sequence[ModelField], model_name: str -) -> type[BaseModel]: - BodyModel = create_model(model_name) - for f in fields: - BodyModel.__fields__[f.name] = f - return BodyModel - - -def get_model_fields(model: type[BaseModel]) -> list[ModelField]: - return list(model.__fields__.values()) diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index cbcb98e1a2..25b6814536 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -4,6 +4,7 @@ from collections.abc import Sequence 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, @@ -11,7 +12,7 @@ from typing import ( 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 @@ -175,7 +176,7 @@ class ModelField: 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 ) @@ -210,22 +211,6 @@ class ModelField: 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", [] @@ -490,6 +475,11 @@ def get_model_fields(model: type[BaseModel]) -> list[ModelField]: 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 @@ -503,22 +493,23 @@ def normalize_name(name: str) -> str: def get_model_name_map(unique_models: TypeModelSet) -> dict[TypeModelOrEnum, str]: name_model_map = {} - conflicting_names: set[str] = set() for model in unique_models: model_name = normalize_name(model.__name__) - if model_name in conflicting_names: - model_name = get_long_model_name(model) - name_model_map[model_name] = model - elif model_name in name_model_map: - conflicting_names.add(model_name) - conflicting_model = name_model_map.pop(model_name) - name_model_map[get_long_model_name(conflicting_model)] = conflicting_model - name_model_map[get_long_model_name(model)] = model - else: - name_model_map[model_name] = model + name_model_map[model_name] = model return {v: k for k, v in name_model_map.items()} +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: @@ -567,5 +558,11 @@ def get_flat_models_from_fields( 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 diff --git a/fastapi/datastructures.py b/fastapi/datastructures.py index 492cbfcccb..2bf5fdb262 100644 --- a/fastapi/datastructures.py +++ b/fastapi/datastructures.py @@ -1,3 +1,4 @@ +from collections.abc import Mapping from typing import ( Annotated, Any, @@ -9,11 +10,7 @@ from typing import ( ) from annotated_doc import Doc -from fastapi._compat import ( - CoreSchema, - GetJsonSchemaHandler, - JsonSchemaValue, -) +from pydantic import GetJsonSchemaHandler from starlette.datastructures import URL as URL # noqa: F401 from starlette.datastructures import Address as Address # noqa: F401 from starlette.datastructures import FormData as FormData # noqa: F401 @@ -142,14 +139,14 @@ class UploadFile(StarletteUploadFile): @classmethod def __get_pydantic_json_schema__( - cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler - ) -> JsonSchemaValue: + cls, core_schema: Mapping[str, Any], handler: GetJsonSchemaHandler + ) -> dict[str, Any]: return {"type": "string", "format": "binary"} @classmethod def __get_pydantic_core_schema__( - cls, source: type[Any], handler: Callable[[Any], CoreSchema] - ) -> CoreSchema: + cls, source: type[Any], handler: Callable[[Any], Mapping[str, Any]] + ) -> Mapping[str, Any]: from ._compat.v2 import with_info_plain_validator_function return with_info_plain_validator_function(cls._validate) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index af2bed9ad9..45e1ff3ed1 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,7 +1,6 @@ import dataclasses import inspect import sys -import warnings from collections.abc import Coroutine, Mapping, Sequence from contextlib import AsyncExitStack, contextmanager from copy import copy, deepcopy @@ -22,13 +21,11 @@ from fastapi._compat import ( 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, @@ -39,19 +36,17 @@ from fastapi._compat import ( 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 @@ -72,8 +67,6 @@ from starlette.responses import Response from starlette.websockets import WebSocket from typing_extensions import Literal, get_args, get_origin -from .. import temp_pydantic_v1_params - multipart_not_installed_error = ( 'Form data requires "python-multipart" to be installed. \n' 'You can install "python-multipart" with: \n\n' @@ -189,7 +182,7 @@ def _get_flat_fields_from_params(fields: list[ModelField]) -> list[ModelField]: 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 @@ -323,16 +316,7 @@ def get_dependant( ) continue assert param_details.field is not None - if isinstance(param_details.field, may_v1.ModelField): - warnings.warn( - "pydantic.v1 is deprecated and will soon stop being supported by FastAPI." - f" Please update the param {param_name}: {param_details.type_annotation!r}.", - category=FastAPIDeprecationWarning, - stacklevel=5, - ) - if isinstance( - param_details.field.field_info, (params.Body, temp_pydantic_v1_params.Body) - ): + if isinstance(param_details.field.field_info, params.Body): dependant.body_params.append(param_details.field) else: add_param_to_fields(field=param_details.field, dependant=dependant) @@ -391,7 +375,7 @@ def analyze_param( fastapi_annotations = [ arg for arg in annotated_args[1:] - if isinstance(arg, (FieldInfo, may_v1.FieldInfo, params.Depends)) + if isinstance(arg, (FieldInfo, params.Depends)) ] fastapi_specific_annotations = [ arg @@ -400,30 +384,27 @@ def analyze_param( arg, ( params.Param, - temp_pydantic_v1_params.Param, params.Body, - temp_pydantic_v1_params.Body, params.Depends, ), ) ] if fastapi_specific_annotations: - fastapi_annotation: Union[ - FieldInfo, may_v1.FieldInfo, params.Depends, None - ] = fastapi_specific_annotations[-1] + fastapi_annotation: Union[FieldInfo, params.Depends, None] = ( + fastapi_specific_annotations[-1] + ) else: fastapi_annotation = None # Set default for Annotated FieldInfo - if isinstance(fastapi_annotation, (FieldInfo, may_v1.FieldInfo)): + if isinstance(fastapi_annotation, FieldInfo): # Copy `field_info` because we mutate `field_info.default` below. field_info = copy_field_info( field_info=fastapi_annotation, # type: ignore[arg-type] annotation=use_annotation, ) - assert field_info.default in { - Undefined, - may_v1.Undefined, - } or field_info.default in {RequiredParam, may_v1.RequiredParam}, ( + assert ( + field_info.default == Undefined or field_info.default == RequiredParam + ), ( f"`{field_info.__class__.__name__}` default value cannot be set in" f" `Annotated` for {param_name!r}. Set the default value with `=` instead." ) @@ -447,7 +428,7 @@ def analyze_param( ) depends = value # Get FieldInfo from default value - elif isinstance(value, (FieldInfo, may_v1.FieldInfo)): + elif isinstance(value, FieldInfo): assert field_info is None, ( "Cannot specify FastAPI annotations in `Annotated` and default value" f" together for {param_name!r}" @@ -491,14 +472,7 @@ def analyze_param( ) or is_uploadfile_sequence_annotation(type_annotation): field_info = params.File(annotation=use_annotation, default=default_value) elif not field_annotation_is_scalar(annotation=type_annotation): - if annotation_is_pydantic_v1(use_annotation): - field_info = temp_pydantic_v1_params.Body( # type: ignore[assignment] - annotation=use_annotation, default=default_value - ) - else: - field_info = params.Body( - annotation=use_annotation, default=default_value - ) + field_info = params.Body(annotation=use_annotation, default=default_value) else: field_info = params.Query(annotation=use_annotation, default=default_value) @@ -507,23 +481,17 @@ def analyze_param( if field_info is not None: # Handle field_info.in_ if is_path_param: - assert isinstance( - field_info, (params.Path, temp_pydantic_v1_params.Path) - ), ( + assert isinstance(field_info, params.Path), ( f"Cannot use `{field_info.__class__.__name__}` for path param" f" {param_name!r}" ) elif ( - isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)) + isinstance(field_info, params.Param) and getattr(field_info, "in_", None) is None ): field_info.in_ = params.ParamTypes.query - 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("_", "-") @@ -535,20 +503,19 @@ def analyze_param( type_=use_annotation_from_field_info, default=field_info.default, alias=alias, - required=field_info.default - in (RequiredParam, may_v1.RequiredParam, Undefined), + required=field_info.default in (RequiredParam, Undefined), field_info=field_info, ) if is_path_param: assert is_scalar_field(field=field), ( "Path params must be of one of the supported types" ) - elif isinstance(field_info, (params.Query, temp_pydantic_v1_params.Query)): + elif isinstance(field_info, params.Query): assert ( is_scalar_field(field) or is_scalar_sequence_field(field) or ( - _is_model_class(field.type_) + lenient_issubclass(field.type_, BaseModel) # For Pydantic v1 and getattr(field, "shape", 1) == 1 ) @@ -742,10 +709,8 @@ def _validate_value_with_model_field( else: return deepcopy(field.default), [] v_, errors_ = field.validate(value, values, loc=loc) - if _is_error_wrapper(errors_): # type: ignore[arg-type] - return None, [errors_] - elif isinstance(errors_, list): - 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_, [] @@ -762,7 +727,7 @@ def _get_multidict_value( if ( value is None or ( - isinstance(field.field_info, (params.Form, temp_pydantic_v1_params.Form)) + isinstance(field.field_info, params.Form) and isinstance(value, str) # For type checks and value == "" ) @@ -832,7 +797,7 @@ def request_params_to_args( if single_not_embedded_field: field_info = first_field.field_info - assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), ( + assert isinstance(field_info, params.Param), ( "Params must be subclasses of Param" ) loc: tuple[str, ...] = (field_info.in_.value,) @@ -844,7 +809,7 @@ def request_params_to_args( for field in fields: value = _get_multidict_value(field, received_params) field_info = field.field_info - assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), ( + assert isinstance(field_info, params.Param), ( "Params must be subclasses of Param" ) loc = (field_info.in_.value, get_validation_alias(field)) @@ -871,7 +836,7 @@ def is_union_of_base_models(field_type: Any) -> bool: 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 @@ -893,8 +858,8 @@ def _should_embed_body_fields(fields: list[ModelField]) -> bool: # If it's a Form (or File) field, it has to be a BaseModel (or a union of BaseModels) to be top level # otherwise it has to be embedded, so that the key value pair can be extracted if ( - isinstance(first_field.field_info, (params.Form, temp_pydantic_v1_params.Form)) - 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 @@ -911,14 +876,14 @@ async def _extract_form_body( value = _get_multidict_value(field, received_body) field_info = field.field_info if ( - isinstance(field_info, (params.File, temp_pydantic_v1_params.File)) + isinstance(field_info, params.File) and is_bytes_field(field) and isinstance(value, UploadFile) ): value = await value.read() elif ( is_bytes_sequence_field(field) - and isinstance(field_info, (params.File, temp_pydantic_v1_params.File)) + and isinstance(field_info, params.File) and value_is_sequence(value) ): # For types @@ -964,7 +929,7 @@ async def request_body_to_args( 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_) @@ -1029,28 +994,15 @@ def get_body_field( BodyFieldInfo_kwargs["default"] = None if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params): BodyFieldInfo: type[params.Body] = params.File - elif any( - isinstance(f.field_info, temp_pydantic_v1_params.File) - for f in flat_dependant.body_params - ): - BodyFieldInfo: type[temp_pydantic_v1_params.Body] = temp_pydantic_v1_params.File # type: ignore[no-redef] elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params): BodyFieldInfo = params.Form - elif any( - isinstance(f.field_info, temp_pydantic_v1_params.Form) - for f in flat_dependant.body_params - ): - BodyFieldInfo = temp_pydantic_v1_params.Form # type: ignore[assignment] else: - if annotation_is_pydantic_v1(BodyModel): - BodyFieldInfo = temp_pydantic_v1_params.Body # type: ignore[assignment] - else: - BodyFieldInfo = params.Body + BodyFieldInfo = params.Body body_param_media_types = [ f.field_info.media_type for f in flat_dependant.body_params - if isinstance(f.field_info, (params.Body, temp_pydantic_v1_params.Body)) + if isinstance(f.field_info, params.Body) ] if len(set(body_param_media_types)) == 1: BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0] diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 549da32797..e8610c983b 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -18,14 +18,18 @@ from typing import Annotated, Any, Callable, Optional, Union from uuid import UUID from annotated_doc import Doc -from fastapi._compat import may_v1 +from fastapi.exceptions import PydanticV1NotSupportedError from fastapi.types import IncEx from pydantic import BaseModel from pydantic.color import Color from pydantic.networks import AnyUrl, NameEmail from pydantic.types import SecretBytes, SecretStr +from 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 @@ -63,7 +67,6 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]: ENCODERS_BY_TYPE: dict[type[Any], Callable[[Any], Any]] = { bytes: lambda o: o.decode(), Color: str, - may_v1.Color: str, datetime.date: isoformat, datetime.datetime: isoformat, datetime.time: isoformat, @@ -80,19 +83,14 @@ ENCODERS_BY_TYPE: dict[type[Any], Callable[[Any], Any]] = { IPv6Interface: str, IPv6Network: str, NameEmail: str, - may_v1.NameEmail: str, Path: str, Pattern: lambda o: o.pattern, SecretBytes: str, - may_v1.SecretBytes: str, SecretStr: str, - may_v1.SecretStr: str, set: list, UUID: str, Url: str, - may_v1.Url: str, AnyUrl: str, - may_v1.AnyUrl: str, } @@ -224,15 +222,8 @@ def jsonable_encoder( include = set(include) if exclude is not None and not isinstance(exclude, (set, dict)): exclude = set(exclude) - if isinstance(obj, (BaseModel, may_v1.BaseModel)): - # TODO: remove when deprecating Pydantic v1 - encoders: dict[Any, Any] = {} - if isinstance(obj, may_v1.BaseModel): - encoders = getattr(obj.__config__, "json_encoders", {}) - if custom_encoder: - encoders = {**encoders, **custom_encoder} - obj_dict = _model_dump( - obj, # type: ignore[arg-type] + if isinstance(obj, BaseModel): + obj_dict = obj.model_dump( mode="json", include=include, exclude=exclude, @@ -241,14 +232,10 @@ def jsonable_encoder( exclude_none=exclude_none, exclude_defaults=exclude_defaults, ) - if "__root__" in obj_dict: - obj_dict = obj_dict["__root__"] return jsonable_encoder( obj_dict, exclude_none=exclude_none, exclude_defaults=exclude_defaults, - # TODO: remove when deprecating Pydantic v1 - custom_encoder=encoders, sqlalchemy_safe=sqlalchemy_safe, ) if dataclasses.is_dataclass(obj): @@ -271,7 +258,7 @@ def jsonable_encoder( 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 = {} @@ -331,7 +318,11 @@ def jsonable_encoder( 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: diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index 53e5052818..1a3abd80c2 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -233,6 +233,12 @@ class ResponseValidationError(ValidationException): self.body = body +class PydanticV1NotSupportedError(FastAPIError): + """ + A pydantic.v1 model is used, which is no longer supported. + """ + + class FastAPIDeprecationWarning(UserWarning): """ A custom deprecation warning as DeprecationWarning is ignored diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index 680f678325..ac6a6d52c3 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -1,15 +1,15 @@ -from collections.abc import Iterable +from collections.abc import Iterable, Mapping from enum import Enum from typing import Annotated, Any, Callable, Optional, Union -from fastapi._compat import ( - CoreSchema, +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 @@ -43,14 +43,14 @@ except ImportError: # pragma: no cover @classmethod def __get_pydantic_json_schema__( - cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler - ) -> JsonSchemaValue: + cls, core_schema: Mapping[str, Any], handler: GetJsonSchemaHandler + ) -> dict[str, Any]: return {"type": "string", "format": "email"} @classmethod def __get_pydantic_core_schema__( - cls, source: type[Any], handler: Callable[[Any], CoreSchema] - ) -> CoreSchema: + cls, source: type[Any], handler: Callable[[Any], Mapping[str, Any]] + ) -> Mapping[str, Any]: return with_info_plain_validator_function(cls._validate) diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 6180fcde6a..75ff261025 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -6,7 +6,6 @@ from typing import Any, Optional, Union, cast from fastapi import routing from fastapi._compat import ( - JsonSchemaValue, ModelField, Undefined, get_compat_model_name_map, @@ -39,8 +38,6 @@ from starlette.responses import JSONResponse from starlette.routing import BaseRoute from typing_extensions import Literal -from .._compat import _is_model_field - validation_error_definition = { "title": "ValidationError", "type": "object", @@ -109,7 +106,7 @@ def _get_openapi_operation_parameters( dependant: Dependant, model_name_map: ModelNameMap, field_mapping: dict[ - tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any] ], separate_input_output_schemas: bool = True, ) -> list[dict[str, Any]]: @@ -182,13 +179,13 @@ def get_openapi_operation_request_body( body_field: Optional[ModelField], model_name_map: ModelNameMap, field_mapping: dict[ - tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any] ], separate_input_output_schemas: bool = True, ) -> Optional[dict[str, Any]]: 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, @@ -265,7 +262,7 @@ def get_openapi_path( operation_ids: set[str], model_name_map: ModelNameMap, field_mapping: dict[ - tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any] ], separate_input_output_schemas: bool = True, ) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]: @@ -457,7 +454,7 @@ def get_fields_from_routes( 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) diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index 844542594b..0834fd741a 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -5,6 +5,7 @@ from annotated_doc import Doc 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 @@ -54,10 +55,8 @@ def Path( # noqa: N802 """ ), ] = _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 @@ -379,10 +378,8 @@ def Query( # noqa: N802 """ ), ] = _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 @@ -683,10 +680,8 @@ def Header( # noqa: N802 """ ), ] = _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 @@ -999,10 +994,8 @@ def Cookie( # noqa: N802 """ ), ] = _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 @@ -1326,10 +1319,8 @@ def Body( # noqa: N802 """ ), ] = _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 @@ -1641,10 +1632,8 @@ def Form( # noqa: N802 """ ), ] = _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 @@ -1955,10 +1944,8 @@ def File( # noqa: N802 """ ), ] = _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 diff --git a/fastapi/params.py b/fastapi/params.py index cc2934f44d..72e797f833 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -6,6 +6,7 @@ from typing import Annotated, Any, Callable, Optional, Union 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 @@ -34,9 +35,7 @@ class Param(FieldInfo): # type: ignore[misc] 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, @@ -147,9 +146,7 @@ class Path(Param): # type: ignore[misc] 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, @@ -233,9 +230,7 @@ class Query(Param): # type: ignore[misc] 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, @@ -317,9 +312,7 @@ class Header(Param): # type: ignore[misc] 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, @@ -403,9 +396,7 @@ class Cookie(Param): # type: ignore[misc] 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, @@ -487,9 +478,7 @@ class Body(FieldInfo): # type: ignore[misc] 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, @@ -600,9 +589,7 @@ class Form(Body): # type: ignore[misc] 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, @@ -684,9 +671,7 @@ class File(Form): # type: ignore[misc] 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, diff --git a/fastapi/routing.py b/fastapi/routing.py index 3f78e93e84..9ca2f46732 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -2,7 +2,6 @@ import email.message import functools import inspect import json -import warnings from collections.abc import ( AsyncIterator, Awaitable, @@ -22,16 +21,12 @@ from typing import ( ) from annotated_doc import Doc -from fastapi import params, temp_pydantic_v1_params +from fastapi import params from fastapi._compat import ( ModelField, Undefined, - _get_model_config, - _model_dump, - _normalize_errors, annotation_is_pydantic_v1, lenient_issubclass, - may_v1, ) from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.dependencies.models import Dependant @@ -47,8 +42,8 @@ from fastapi.dependencies.utils import ( from fastapi.encoders import jsonable_encoder from fastapi.exceptions import ( EndpointContext, - FastAPIDeprecationWarning, FastAPIError, + PydanticV1NotSupportedError, RequestValidationError, ResponseValidationError, WebSocketRequestValidationError, @@ -148,51 +143,6 @@ def websocket_session( return app -def _prepare_response_content( - res: Any, - *, - exclude_unset: bool, - exclude_defaults: bool = False, - exclude_none: bool = False, -) -> Any: - if isinstance(res, may_v1.BaseModel): - read_with_orm_mode = getattr(_get_model_config(res), "read_with_orm_mode", None) # type: ignore[arg-type] - if read_with_orm_mode: - # Let from_orm extract the data from this model instead of converting - # it now to a dict. - # Otherwise, there's no way to extract lazy data that requires attribute - # access instead of dict iteration, e.g. lazy relationships. - return res - return _model_dump( - res, # type: ignore[arg-type] - by_alias=True, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - elif isinstance(res, list): - return [ - _prepare_response_content( - item, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - for item in res - ] - elif isinstance(res, dict): - return { - k: _prepare_response_content( - v, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - for k, v in res.items() - } - return res - - def _merge_lifespan_context( original_context: Lifespan[Any], nested_context: Lifespan[Any] ) -> Lifespan[Any]: @@ -252,14 +202,6 @@ async def serialize_response( ) -> Any: if field: errors = [] - if not hasattr(field, "serialize"): - # pydantic v1 - response_content = _prepare_response_content( - response_content, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) if is_coroutine: value, errors_ = field.validate(response_content, {}, loc=("response",)) else: @@ -268,28 +210,15 @@ async def serialize_response( ) if isinstance(errors_, list): errors.extend(errors_) - elif errors_: - errors.append(errors_) if errors: ctx = endpoint_ctx or EndpointContext() raise ResponseValidationError( - errors=_normalize_errors(errors), + errors=errors, body=response_content, endpoint_ctx=ctx, ) - if hasattr(field, "serialize"): - return field.serialize( - value, - include=include, - exclude=exclude, - by_alias=by_alias, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - - return jsonable_encoder( + return field.serialize( value, include=include, exclude=exclude, @@ -298,6 +227,7 @@ async def serialize_response( exclude_defaults=exclude_defaults, exclude_none=exclude_none, ) + else: return jsonable_encoder(response_content) @@ -332,9 +262,7 @@ def get_request_handler( ) -> Callable[[Request], Coroutine[Any, Any, Response]]: assert dependant.call is not None, "dependant.call must be a function" is_coroutine = dependant.is_coroutine_callable - is_body_form = body_field and isinstance( - body_field.field_info, (params.Form, temp_pydantic_v1_params.Form) - ) + is_body_form = body_field and isinstance(body_field.field_info, params.Form) if isinstance(response_class, DefaultPlaceholder): actual_response_class: type[Response] = response_class.value else: @@ -464,7 +392,7 @@ def get_request_handler( response.headers.raw.extend(solved_result.response.headers.raw) if errors: validation_error = RequestValidationError( - _normalize_errors(errors), body=body, endpoint_ctx=endpoint_ctx + errors, body=body, endpoint_ctx=endpoint_ctx ) raise validation_error @@ -503,7 +431,7 @@ def get_websocket_app( ) if solved_result.errors: raise WebSocketRequestValidationError( - _normalize_errors(solved_result.errors), + solved_result.errors, endpoint_ctx=endpoint_ctx, ) assert dependant.call is not None, "dependant.call must be a function" @@ -638,11 +566,9 @@ class APIRoute(routing.Route): ) response_name = "Response_" + self.unique_id if annotation_is_pydantic_v1(self.response_model): - warnings.warn( - "pydantic.v1 is deprecated and will soon stop being supported by FastAPI." - f" Please update the response model {self.response_model!r}.", - category=FastAPIDeprecationWarning, - stacklevel=4, + raise PydanticV1NotSupportedError( + "pydantic.v1 models are no longer supported by FastAPI." + f" Please update the response model {self.response_model!r}." ) self.response_field = create_model_field( name=response_name, @@ -678,11 +604,9 @@ class APIRoute(routing.Route): ) response_name = f"Response_{additional_status_code}_{self.unique_id}" if annotation_is_pydantic_v1(model): - warnings.warn( - "pydantic.v1 is deprecated and will soon stop being supported by FastAPI." - f" In responses={{}}, please update {model}.", - category=FastAPIDeprecationWarning, - stacklevel=4, + raise PydanticV1NotSupportedError( + "pydantic.v1 models are no longer supported by FastAPI." + f" In responses={{}}, please update {model}." ) response_field = create_model_field( name=response_name, type_=model, mode="serialization" diff --git a/fastapi/temp_pydantic_v1_params.py b/fastapi/temp_pydantic_v1_params.py deleted file mode 100644 index 62230e42cf..0000000000 --- a/fastapi/temp_pydantic_v1_params.py +++ /dev/null @@ -1,718 +0,0 @@ -import warnings -from typing import Annotated, Any, Callable, Optional, Union - -from fastapi.exceptions import FastAPIDeprecationWarning -from fastapi.openapi.models import Example -from fastapi.params import ParamTypes -from typing_extensions import deprecated - -from ._compat.may_v1 import FieldInfo, Undefined - -_Unset: Any = Undefined - - -class Param(FieldInfo): - in_: ParamTypes - - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - if example is not _Unset: - warnings.warn( - "`example` has been deprecated, please use `examples` instead", - category=FastAPIDeprecationWarning, - stacklevel=4, - ) - self.example = example - self.include_in_schema = include_in_schema - self.openapi_examples = openapi_examples - kwargs = dict( - default=default, - default_factory=default_factory, - alias=alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - discriminator=discriminator, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - **extra, - ) - if examples is not None: - kwargs["examples"] = examples - if regex is not None: - warnings.warn( - "`regex` has been deprecated, please use `pattern` instead", - category=FastAPIDeprecationWarning, - stacklevel=4, - ) - current_json_schema_extra = json_schema_extra or extra - kwargs["deprecated"] = deprecated - kwargs["regex"] = pattern or regex - kwargs.update(**current_json_schema_extra) - use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} - - super().__init__(**use_kwargs) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.default})" - - -class Path(Param): - in_ = ParamTypes.path - - def __init__( - self, - default: Any = ..., - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - assert default is ..., "Path parameters cannot have a default value" - self.in_ = self.in_ - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) - - -class Query(Param): - in_ = ParamTypes.query - - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) - - -class Header(Param): - in_ = ParamTypes.header - - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - convert_underscores: bool = True, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - self.convert_underscores = convert_underscores - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) - - -class Cookie(Param): - in_ = ParamTypes.cookie - - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) - - -class Body(FieldInfo): - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - embed: Union[bool, None] = None, - media_type: str = "application/json", - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - self.embed = embed - self.media_type = media_type - if example is not _Unset: - warnings.warn( - "`example` has been deprecated, please use `examples` instead", - category=FastAPIDeprecationWarning, - stacklevel=4, - ) - self.example = example - self.include_in_schema = include_in_schema - self.openapi_examples = openapi_examples - kwargs = dict( - default=default, - default_factory=default_factory, - alias=alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - discriminator=discriminator, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - **extra, - ) - if examples is not None: - kwargs["examples"] = examples - if regex is not None: - warnings.warn( - "`regex` has been deprecated, please use `pattern` instead", - category=FastAPIDeprecationWarning, - stacklevel=4, - ) - current_json_schema_extra = json_schema_extra or extra - kwargs["deprecated"] = deprecated - kwargs["regex"] = pattern or regex - kwargs.update(**current_json_schema_extra) - - use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} - - super().__init__(**use_kwargs) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.default})" - - -class Form(Body): - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - media_type: str = "application/x-www-form-urlencoded", - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - media_type=media_type, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) - - -class File(Form): - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - media_type: str = "multipart/form-data", - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - media_type=media_type, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) diff --git a/fastapi/utils.py b/fastapi/utils.py index 8ae50aa145..78fdcbb5b4 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -6,7 +6,6 @@ from typing import ( Any, Optional, Union, - cast, ) from weakref import WeakKeyDictionary @@ -19,11 +18,9 @@ from fastapi._compat import ( UndefinedType, Validator, annotation_is_pydantic_v1, - lenient_issubclass, - may_v1, ) from fastapi.datastructures import DefaultPlaceholder, DefaultType -from fastapi.exceptions import FastAPIDeprecationWarning +from fastapi.exceptions import FastAPIDeprecationWarning, PydanticV1NotSupportedError from pydantic import BaseModel from pydantic.fields import FieldInfo from typing_extensions import Literal @@ -83,52 +80,18 @@ def create_model_field( mode: Literal["validation", "serialization"] = "validation", version: Literal["1", "auto"] = "auto", ) -> ModelField: - 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 @@ -139,57 +102,7 @@ def create_cloned_field( *, cloned_types: Optional[MutableMapping[type[BaseModel], type[BaseModel]]] = None, ) -> ModelField: - if isinstance(field, v2.ModelField): - return field - - from fastapi._compat import v1 - - # cloned_types caches already cloned types to support recursive models and improve - # performance by avoiding unnecessary cloning - if cloned_types is None: - cloned_types = _CLONED_TYPES_CACHE - - original_type = field.type_ - use_type = original_type - if lenient_issubclass(original_type, v1.BaseModel): - original_type = cast(type[v1.BaseModel], original_type) - use_type = cloned_types.get(original_type) - if use_type is None: - use_type = v1.create_model(original_type.__name__, __base__=original_type) - cloned_types[original_type] = use_type - for f in original_type.__fields__.values(): - use_type.__fields__[f.name] = create_cloned_field( - f, - cloned_types=cloned_types, - ) - new_field = create_model_field(name=field.name, type_=use_type, version="1") - new_field.has_alias = field.has_alias # type: ignore[attr-defined] - new_field.alias = field.alias # type: ignore[misc] - new_field.class_validators = field.class_validators # type: ignore[attr-defined] - new_field.default = field.default # type: ignore[misc] - new_field.default_factory = field.default_factory # type: ignore[attr-defined] - new_field.required = field.required # type: ignore[misc] - new_field.model_config = field.model_config # type: ignore[attr-defined] - new_field.field_info = field.field_info - new_field.allow_none = field.allow_none # type: ignore[attr-defined] - new_field.validate_always = field.validate_always # type: ignore[attr-defined] - if field.sub_fields: # type: ignore[attr-defined] - new_field.sub_fields = [ # type: ignore[attr-defined] - create_cloned_field(sub_field, cloned_types=cloned_types) - for sub_field in field.sub_fields # type: ignore[attr-defined] - ] - if field.key_field: # type: ignore[attr-defined] - new_field.key_field = create_cloned_field( # type: ignore[attr-defined] - field.key_field, # type: ignore[attr-defined] - cloned_types=cloned_types, - ) - new_field.validators = field.validators # type: ignore[attr-defined] - new_field.pre_validators = field.pre_validators # type: ignore[attr-defined] - new_field.post_validators = field.post_validators # type: ignore[attr-defined] - new_field.parse_json = field.parse_json # type: ignore[attr-defined] - new_field.shape = field.shape # type: ignore[attr-defined] - new_field.populate_validators() # type: ignore[attr-defined] - return new_field + return field def generate_operation_id_for_path( diff --git a/pyproject.toml b/pyproject.toml index 8f824af5d5..9c2c35a9f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -199,14 +199,22 @@ omit = [ "docs_src/dependencies/tutorial008_an_py39.py", # difficult to mock "docs_src/dependencies/tutorial013_an_py310.py", # temporary code example? "docs_src/dependencies/tutorial014_an_py310.py", # temporary code example? - # Pydantic V1 + # Pydantic v1 migration, no longer tested + "docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py", + "docs_src/pydantic_v1_in_v2/tutorial001_an_py39.py", + "docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py", + "docs_src/pydantic_v1_in_v2/tutorial002_an_py39.py", + "docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py", + "docs_src/pydantic_v1_in_v2/tutorial003_an_py39.py", + "docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py", + "docs_src/pydantic_v1_in_v2/tutorial004_an_py39.py", + # TODO: remove when removing this file, after updating translations, Pydantic v1 "docs_src/schema_extra_example/tutorial001_pv1_py310.py", - "docs_src/query_param_models/tutorial002_pv1_py310.py", - "docs_src/query_param_models/tutorial002_pv1_an_py310.py", - "docs_src/header_param_models/tutorial002_pv1_py310.py", - "docs_src/header_param_models/tutorial002_pv1_an_py310.py", - "docs_src/cookie_param_models/tutorial002_pv1_py310.py", - "docs_src/cookie_param_models/tutorial002_pv1_an_py310.py", + "docs_src/schema_extra_example/tutorial001_pv1_py39.py", + "docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py", + "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] diff --git a/tests/test_compat.py b/tests/test_compat.py index 8d20710303..0b5600f8f5 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -1,20 +1,16 @@ -from typing import Any, Union +from typing import Union from fastapi import FastAPI, UploadFile from fastapi._compat import ( Undefined, - _get_model_config, - get_cached_model_fields, - is_scalar_field, is_uploadfile_sequence_annotation, - may_v1, ) from fastapi._compat.shared import is_bytes_sequence_annotation from fastapi.testclient import TestClient from pydantic import BaseModel, ConfigDict from pydantic.fields import FieldInfo -from .utils import needs_py310, needs_py_lt_314 +from .utils import needs_py310 def test_model_field_default_required(): @@ -26,35 +22,6 @@ def test_model_field_default_required(): assert field.default is Undefined -@needs_py_lt_314 -def test_v1_plain_validator_function(): - from fastapi._compat import v1 - - # For coverage - def func(v): # pragma: no cover - return v - - result = v1.with_info_plain_validator_function(func) - assert result == {} - - -def test_is_model_field(): - # For coverage - from fastapi._compat import _is_model_field - - 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() @@ -165,33 +132,3 @@ def test_serialize_sequence_value_with_none_first_in_union(): result = v2.serialize_sequence_value(field=field, value=["x", "y"]) assert result == ["x", "y"] assert isinstance(result, list) - - -@needs_py_lt_314 -def test_is_pv1_scalar_field(): - from fastapi._compat import v1 - - # For coverage - class Model(v1.BaseModel): - foo: Union[str, dict[str, Any]] - - fields = v1.get_model_fields(Model) - assert not is_scalar_field(fields[0]) - - -@needs_py_lt_314 -def test_get_model_fields_cached(): - from fastapi._compat import v1 - - class Model(may_v1.BaseModel): - foo: str - - non_cached_fields = v1.get_model_fields(Model) - non_cached_fields2 = v1.get_model_fields(Model) - cached_fields = get_cached_model_fields(Model) - cached_fields2 = get_cached_model_fields(Model) - for f1, f2 in zip(cached_fields, cached_fields2): - assert f1 is f2 - - assert non_cached_fields is not non_cached_fields2 - assert cached_fields is cached_fields2 diff --git a/tests/test_compat_params_v1.py b/tests/test_compat_params_v1.py deleted file mode 100644 index 704b3f77a6..0000000000 --- a/tests/test_compat_params_v1.py +++ /dev/null @@ -1,1060 +0,0 @@ -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", - }, - } - }, - } - ) diff --git a/tests/test_datetime_custom_encoder.py b/tests/test_datetime_custom_encoder.py index 56b6780f04..f154ede029 100644 --- a/tests/test_datetime_custom_encoder.py +++ b/tests/test_datetime_custom_encoder.py @@ -1,12 +1,9 @@ -import warnings from datetime import datetime, timezone from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel -from .utils import needs_pydanticv1 - def test_pydanticv2(): from pydantic import field_serializer @@ -29,34 +26,3 @@ def test_pydanticv2(): with client: response = client.get("/model") assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"} - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_pydanticv1(): - from pydantic import v1 - - class ModelWithDatetimeField(v1.BaseModel): - dt_field: datetime - - class Config: - json_encoders = { - datetime: lambda dt: dt.replace( - microsecond=0, tzinfo=timezone.utc - ).isoformat() - } - - app = FastAPI() - model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8)) - - with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.get("/model", response_model=ModelWithDatetimeField) - def get_model(): - return model - - client = TestClient(app) - with client: - response = client.get("/model") - assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"} diff --git a/tests/test_filter_pydantic_sub_model/__init__.py b/tests/test_filter_pydantic_sub_model/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/test_filter_pydantic_sub_model/app_pv1.py b/tests/test_filter_pydantic_sub_model/app_pv1.py deleted file mode 100644 index d6f2ce7d2d..0000000000 --- a/tests/test_filter_pydantic_sub_model/app_pv1.py +++ /dev/null @@ -1,45 +0,0 @@ -import warnings -from typing import Optional - -from fastapi import Depends, FastAPI -from pydantic.v1 import BaseModel, validator - -app = FastAPI() - - -class ModelB(BaseModel): - username: str - - -class ModelC(ModelB): - password: str - - -class ModelA(BaseModel): - name: str - description: Optional[str] = None - model_b: ModelB - tags: dict[str, str] = {} - - @validator("name") - def lower_username(cls, name: str, values): - if not name.endswith("A"): - raise ValueError("name must end in A") - return name - - -async def get_model_c() -> ModelC: - return ModelC(username="test-user", password="test-password") - - -with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.get("/model/{name}", response_model=ModelA) - async def get_model_a(name: str, model_c=Depends(get_model_c)): - return { - "name": name, - "description": "model-a-desc", - "model_b": model_c, - "tags": {"key1": "value1", "key2": "value2"}, - } diff --git a/tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py b/tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py deleted file mode 100644 index b464b4f572..0000000000 --- a/tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py +++ /dev/null @@ -1,146 +0,0 @@ -import pytest -from fastapi.exceptions import ResponseValidationError -from fastapi.testclient import TestClient -from inline_snapshot import snapshot - -from ..utils import needs_pydanticv1 - - -@pytest.fixture(name="client") -def get_client(): - from .app_pv1 import app - - client = TestClient(app) - return client - - -@needs_pydanticv1 -def test_filter_sub_model(client: TestClient): - response = client.get("/model/modelA") - assert response.status_code == 200, response.text - assert response.json() == { - "name": "modelA", - "description": "model-a-desc", - "model_b": {"username": "test-user"}, - "tags": {"key1": "value1", "key2": "value2"}, - } - - -@needs_pydanticv1 -def test_validator_is_cloned(client: TestClient): - with pytest.raises(ResponseValidationError) as err: - client.get("/model/modelX") - assert err.value.errors() == [ - { - "loc": ("response", "name"), - "msg": "name must end in A", - "type": "value_error", - } - ] - - -@needs_pydanticv1 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/model/{name}": { - "get": { - "summary": "Get Model A", - "operationId": "get_model_a_model__name__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Name", "type": "string"}, - "name": "name", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ModelA" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "ModelA": { - "title": "ModelA", - "required": ["name", "model_b"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "model_b": {"$ref": "#/components/schemas/ModelB"}, - "tags": { - "additionalProperties": {"type": "string"}, - "type": "object", - "title": "Tags", - "default": {}, - }, - }, - }, - "ModelB": { - "title": "ModelB", - "required": ["username"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"} - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } - ) diff --git a/tests/test_get_model_definitions_formfeed_escape.py b/tests/test_get_model_definitions_formfeed_escape.py index dee5955544..eb7939b69a 100644 --- a/tests/test_get_model_definitions_formfeed_escape.py +++ b/tests/test_get_model_definitions_formfeed_escape.py @@ -1,25 +1,12 @@ -import warnings - import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from inline_snapshot import snapshot -from .utils import needs_pydanticv1 - -@pytest.fixture( - name="client", - params=[ - pytest.param("pydantic-v1", marks=needs_pydanticv1), - "pydantic-v2", - ], -) -def client_fixture(request: pytest.FixtureRequest) -> TestClient: - if request.param == "pydantic-v1": - from pydantic.v1 import BaseModel - else: - from pydantic import BaseModel +@pytest.fixture(name="client") +def client_fixture() -> TestClient: + from pydantic import BaseModel class Address(BaseModel): """ @@ -38,28 +25,12 @@ def client_fixture(request: pytest.FixtureRequest) -> TestClient: app = FastAPI() - if request.param == "pydantic-v1": - with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.get("/facilities/{facility_id}") - def get_facility(facility_id: str) -> Facility: - return Facility( - id=facility_id, - address=Address( - line_1="123 Main St", city="Anytown", state_province="CA" - ), - ) - else: - - @app.get("/facilities/{facility_id}") - def get_facility(facility_id: str) -> Facility: - return Facility( - id=facility_id, - address=Address( - line_1="123 Main St", city="Anytown", state_province="CA" - ), - ) + @app.get("/facilities/{facility_id}") + def get_facility(facility_id: str) -> Facility: + return Facility( + id=facility_id, + address=Address(line_1="123 Main St", city="Anytown", state_province="CA"), + ) client = TestClient(app) return client diff --git a/tests/test_inherited_custom_class.py b/tests/test_inherited_custom_class.py index 7f29fe33ed..8cf8952f92 100644 --- a/tests/test_inherited_custom_class.py +++ b/tests/test_inherited_custom_class.py @@ -5,8 +5,6 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel -from .utils import needs_pydanticv1 - class MyUuid: def __init__(self, uuid_string: str): @@ -67,46 +65,3 @@ def test_pydanticv2(): assert response_pydantic.json() == { "a_uuid": "b8799909-f914-42de-91bc-95c819218d01" } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_pydanticv1(): - from pydantic import v1 - - app = FastAPI() - - @app.get("/fast_uuid") - def return_fast_uuid(): - asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51") - assert isinstance(asyncpg_uuid, uuid.UUID) - assert type(asyncpg_uuid) is not uuid.UUID - with pytest.raises(TypeError): - vars(asyncpg_uuid) - return {"fast_uuid": asyncpg_uuid} - - class SomeCustomClass(v1.BaseModel): - class Config: - arbitrary_types_allowed = True - json_encoders = {uuid.UUID: str} - - a_uuid: MyUuid - - @app.get("/get_custom_class") - def return_some_user(): - # Test that the fix also works for custom pydantic classes - return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01")) - - client = TestClient(app) - - with client: - response_simple = client.get("/fast_uuid") - response_pydantic = client.get("/get_custom_class") - - assert response_simple.json() == { - "fast_uuid": "a10ff360-3b1e-4984-a26f-d3ab460bdb51" - } - - assert response_pydantic.json() == { - "a_uuid": "b8799909-f914-42de-91bc-95c819218d01" - } diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index 81bf94ece0..4528dff440 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -1,3 +1,4 @@ +import warnings from collections import deque from dataclasses import dataclass from datetime import datetime, timezone @@ -5,15 +6,14 @@ from decimal import Decimal from enum import Enum from math import isinf, isnan from pathlib import PurePath, PurePosixPath, PureWindowsPath -from typing import Optional +from typing import Optional, TypedDict import pytest from fastapi._compat import Undefined from fastapi.encoders import jsonable_encoder +from fastapi.exceptions import PydanticV1NotSupportedError from pydantic import BaseModel, Field, ValidationError -from .utils import needs_pydanticv1 - class Person: def __init__(self, name: str): @@ -156,29 +156,17 @@ def test_encode_custom_json_encoders_model_pydanticv2(): assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_encode_custom_json_encoders_model_pydanticv1(): - from pydantic import v1 - - 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(): @@ -214,25 +202,27 @@ def test_encode_model_with_default(): } -@needs_pydanticv1 def test_custom_encoders(): - from pydantic import v1 - class safe_datetime(datetime): pass - class MyModel(v1.BaseModel): + class MyDict(TypedDict): dt_field: safe_datetime - instance = MyModel(dt_field=safe_datetime.now()) + instance = MyDict(dt_field=safe_datetime.now()) encoded_instance = jsonable_encoder( instance, custom_encoder={safe_datetime: lambda o: o.strftime("%H:%M:%S")} ) - assert encoded_instance["dt_field"] == instance.dt_field.strftime("%H:%M:%S") + assert encoded_instance["dt_field"] == instance["dt_field"].strftime("%H:%M:%S") + + encoded_instance = jsonable_encoder( + instance, custom_encoder={datetime: lambda o: o.strftime("%H:%M:%S")} + ) + assert encoded_instance["dt_field"] == instance["dt_field"].strftime("%H:%M:%S") encoded_instance2 = jsonable_encoder(instance) - assert encoded_instance2["dt_field"] == instance.dt_field.isoformat() + assert encoded_instance2["dt_field"] == instance["dt_field"].isoformat() def test_custom_enum_encoders(): @@ -287,17 +277,6 @@ def test_encode_pure_path(): assert jsonable_encoder({"path": test_path}) == {"path": str(test_path)} -@needs_pydanticv1 -def test_encode_root(): - from pydantic import v1 - - class ModelWithRoot(v1.BaseModel): - __root__: str - - model = ModelWithRoot(__root__="Foo") - assert jsonable_encoder(model) == "Foo" - - def test_decimal_encoder_float(): data = {"value": Decimal(1.23)} assert jsonable_encoder(data) == {"value": 1.23} diff --git a/tests/test_pydantic_v1_deprecation_warnings.py b/tests/test_pydantic_v1_deprecation_warnings.py deleted file mode 100644 index 89ca6a8658..0000000000 --- a/tests/test_pydantic_v1_deprecation_warnings.py +++ /dev/null @@ -1,99 +0,0 @@ -import sys - -import pytest -from fastapi.exceptions import FastAPIDeprecationWarning - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi import FastAPI -from fastapi._compat.v1 import BaseModel -from fastapi.testclient import TestClient - - -def test_warns_pydantic_v1_model_in_endpoint_param() -> None: - class ParamModelV1(BaseModel): - name: str - - app = FastAPI() - - with pytest.warns( - FastAPIDeprecationWarning, - match=r"pydantic\.v1 is deprecated.*Please update the param data:", - ): - - @app.post("/param") - def endpoint(data: ParamModelV1): - return data - - client = TestClient(app) - response = client.post("/param", json={"name": "test"}) - assert response.status_code == 200, response.text - assert response.json() == {"name": "test"} - - -def test_warns_pydantic_v1_model_in_return_type() -> None: - class ReturnModelV1(BaseModel): - name: str - - app = FastAPI() - - with pytest.warns( - FastAPIDeprecationWarning, - match=r"pydantic\.v1 is deprecated.*Please update the response model", - ): - - @app.get("/return") - def endpoint() -> ReturnModelV1: - return ReturnModelV1(name="test") - - client = TestClient(app) - response = client.get("/return") - assert response.status_code == 200, response.text - assert response.json() == {"name": "test"} - - -def test_warns_pydantic_v1_model_in_response_model() -> None: - class ResponseModelV1(BaseModel): - name: str - - app = FastAPI() - - with pytest.warns( - FastAPIDeprecationWarning, - match=r"pydantic\.v1 is deprecated.*Please update the response model", - ): - - @app.get("/response-model", response_model=ResponseModelV1) - def endpoint(): - return {"name": "test"} - - client = TestClient(app) - response = client.get("/response-model") - assert response.status_code == 200, response.text - assert response.json() == {"name": "test"} - - -def test_warns_pydantic_v1_model_in_additional_responses_model() -> None: - class ErrorModelV1(BaseModel): - detail: str - - app = FastAPI() - - with pytest.warns( - FastAPIDeprecationWarning, - match=r"pydantic\.v1 is deprecated.*In responses=\{\}, please update", - ): - - @app.get( - "/responses", response_model=None, responses={400: {"model": ErrorModelV1}} - ) - def endpoint(): - return {"ok": True} - - client = TestClient(app) - response = client.get("/responses") - assert response.status_code == 200, response.text - assert response.json() == {"ok": True} diff --git a/tests/test_pydantic_v1_error.py b/tests/test_pydantic_v1_error.py new file mode 100644 index 0000000000..13229a3137 --- /dev/null +++ b/tests/test_pydantic_v1_error.py @@ -0,0 +1,97 @@ +import sys +import warnings +from typing import Union + +import pytest + +from tests.utils import skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + +from fastapi import FastAPI +from fastapi.exceptions import PydanticV1NotSupportedError + +with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + from pydantic.v1 import BaseModel + + +def test_raises_pydantic_v1_model_in_endpoint_param() -> None: + class ParamModelV1(BaseModel): + name: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.post("/param") + def endpoint(data: ParamModelV1): # pragma: no cover + return data + + +def test_raises_pydantic_v1_model_in_return_type() -> None: + class ReturnModelV1(BaseModel): + name: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.get("/return") + def endpoint() -> ReturnModelV1: # pragma: no cover + return ReturnModelV1(name="test") + + +def test_raises_pydantic_v1_model_in_response_model() -> None: + class ResponseModelV1(BaseModel): + name: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.get("/response-model", response_model=ResponseModelV1) + def endpoint(): # pragma: no cover + return {"name": "test"} + + +def test_raises_pydantic_v1_model_in_additional_responses_model() -> None: + class ErrorModelV1(BaseModel): + detail: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.get( + "/responses", response_model=None, responses={400: {"model": ErrorModelV1}} + ) + def endpoint(): # pragma: no cover + return {"ok": True} + + +def test_raises_pydantic_v1_model_in_union() -> None: + class ModelV1A(BaseModel): + name: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.post("/union") + def endpoint(data: Union[dict, ModelV1A]): # pragma: no cover + return data + + +def test_raises_pydantic_v1_model_in_sequence() -> None: + class ModelV1A(BaseModel): + name: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.post("/sequence") + def endpoint(data: list[ModelV1A]): # pragma: no cover + return data diff --git a/tests/test_pydantic_v1_v2_01.py b/tests/test_pydantic_v1_v2_01.py deleted file mode 100644 index 4868e5d223..0000000000 --- a/tests/test_pydantic_v1_v2_01.py +++ /dev/null @@ -1,439 +0,0 @@ -import sys -import warnings -from typing import Any, Union - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi import FastAPI -from fastapi._compat.v1 import BaseModel -from fastapi.testclient import TestClient -from inline_snapshot import snapshot - - -class SubItem(BaseModel): - name: str - - -class Item(BaseModel): - title: str - size: int - description: Union[str, None] = None - sub: SubItem - multi: list[SubItem] = [] - - -app = FastAPI() - -with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.post("/simple-model") - def handle_simple_model(data: SubItem) -> SubItem: - return data - - @app.post("/simple-model-filter", response_model=SubItem) - def handle_simple_model_filter(data: SubItem) -> Any: - extended_data = data.dict() - extended_data.update({"secret_price": 42}) - return extended_data - - @app.post("/item") - def handle_item(data: Item) -> Item: - return data - - @app.post("/item-filter", response_model=Item) - def handle_item_filter(data: Item) -> Any: - extended_data = data.dict() - extended_data.update({"secret_data": "classified", "internal_id": 12345}) - extended_data["sub"].update({"internal_id": 67890}) - return extended_data - - -client = TestClient(app) - - -def test_old_simple_model(): - response = client.post( - "/simple-model", - json={"name": "Foo"}, - ) - assert response.status_code == 200, response.text - assert response.json() == {"name": "Foo"} - - -def test_old_simple_model_validation_error(): - response = client.post( - "/simple-model", - json={"wrong_name": "Foo"}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "name"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_old_simple_model_filter(): - response = client.post( - "/simple-model-filter", - json={"name": "Foo"}, - ) - assert response.status_code == 200, response.text - assert response.json() == {"name": "Foo"} - - -def test_item_model(): - response = client.post( - "/item", - json={ - "title": "Test Item", - "size": 100, - "description": "This is a test item", - "sub": {"name": "SubItem1"}, - "multi": [{"name": "Multi1"}, {"name": "Multi2"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "Test Item", - "size": 100, - "description": "This is a test item", - "sub": {"name": "SubItem1"}, - "multi": [{"name": "Multi1"}, {"name": "Multi2"}], - } - - -def test_item_model_minimal(): - response = client.post( - "/item", - json={"title": "Minimal Item", "size": 50, "sub": {"name": "SubMin"}}, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "Minimal Item", - "size": 50, - "description": None, - "sub": {"name": "SubMin"}, - "multi": [], - } - - -def test_item_model_validation_errors(): - response = client.post( - "/item", - json={"title": "Missing fields"}, - ) - assert response.status_code == 422, response.text - error_detail = response.json()["detail"] - assert len(error_detail) == 2 - assert { - "loc": ["body", "size"], - "msg": "field required", - "type": "value_error.missing", - } in error_detail - assert { - "loc": ["body", "sub"], - "msg": "field required", - "type": "value_error.missing", - } in error_detail - - -def test_item_model_nested_validation_error(): - response = client.post( - "/item", - json={"title": "Test Item", "size": 100, "sub": {"wrong_field": "test"}}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "sub", "name"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_item_model_invalid_type(): - response = client.post( - "/item", - json={"title": "Test Item", "size": "not_a_number", "sub": {"name": "SubItem"}}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "size"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) - - -def test_item_filter(): - response = client.post( - "/item-filter", - json={ - "title": "Filtered Item", - "size": 200, - "description": "Test filtering", - "sub": {"name": "SubFiltered"}, - "multi": [], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == { - "title": "Filtered Item", - "size": 200, - "description": "Test filtering", - "sub": {"name": "SubFiltered"}, - "multi": [], - } - assert "secret_data" not in result - assert "internal_id" not in result - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/simple-model": { - "post": { - "summary": "Handle Simple Model", - "operationId": "handle_simple_model_simple_model_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/SubItem"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubItem" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/simple-model-filter": { - "post": { - "summary": "Handle Simple Model Filter", - "operationId": "handle_simple_model_filter_simple_model_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/SubItem"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubItem" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item": { - "post": { - "summary": "Handle Item", - "operationId": "handle_item_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item-filter": { - "post": { - "summary": "Handle Item Filter", - "operationId": "handle_item_filter_item_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "title": {"type": "string", "title": "Title"}, - "size": {"type": "integer", "title": "Size"}, - "description": {"type": "string", "title": "Description"}, - "sub": {"$ref": "#/components/schemas/SubItem"}, - "multi": { - "items": {"$ref": "#/components/schemas/SubItem"}, - "type": "array", - "title": "Multi", - "default": [], - }, - }, - "type": "object", - "required": ["title", "size", "sub"], - "title": "Item", - }, - "SubItem": { - "properties": {"name": {"type": "string", "title": "Name"}}, - "type": "object", - "required": ["name"], - "title": "SubItem", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_pydantic_v1_v2_list.py b/tests/test_pydantic_v1_v2_list.py deleted file mode 100644 index 108f231faa..0000000000 --- a/tests/test_pydantic_v1_v2_list.py +++ /dev/null @@ -1,682 +0,0 @@ -import sys -import warnings -from typing import Any, Union - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi import FastAPI -from fastapi._compat.v1 import BaseModel -from fastapi.testclient import TestClient -from inline_snapshot import snapshot - - -class SubItem(BaseModel): - name: str - - -class Item(BaseModel): - title: str - size: int - description: Union[str, None] = None - sub: SubItem - multi: list[SubItem] = [] - - -app = FastAPI() - - -with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.post("/item") - def handle_item(data: Item) -> list[Item]: - return [data, data] - - @app.post("/item-filter", response_model=list[Item]) - def handle_item_filter(data: Item) -> Any: - extended_data = data.dict() - extended_data.update({"secret_data": "classified", "internal_id": 12345}) - extended_data["sub"].update({"internal_id": 67890}) - return [extended_data, extended_data] - - @app.post("/item-list") - def handle_item_list(data: list[Item]) -> Item: - if data: - return data[0] - return Item(title="", size=0, sub=SubItem(name="")) - - @app.post("/item-list-filter", response_model=Item) - def handle_item_list_filter(data: list[Item]) -> Any: - if data: - extended_data = data[0].dict() - extended_data.update({"secret_data": "classified", "internal_id": 12345}) - extended_data["sub"].update({"internal_id": 67890}) - return extended_data - return Item(title="", size=0, sub=SubItem(name="")) - - @app.post("/item-list-to-list") - def handle_item_list_to_list(data: list[Item]) -> list[Item]: - return data - - @app.post("/item-list-to-list-filter", response_model=list[Item]) - def handle_item_list_to_list_filter(data: list[Item]) -> Any: - if data: - extended_data = data[0].dict() - extended_data.update({"secret_data": "classified", "internal_id": 12345}) - extended_data["sub"].update({"internal_id": 67890}) - return [extended_data, extended_data] - return [] - - -client = TestClient(app) - - -def test_item_to_list(): - response = client.post( - "/item", - json={ - "title": "Test Item", - "size": 100, - "description": "This is a test item", - "sub": {"name": "SubItem1"}, - "multi": [{"name": "Multi1"}, {"name": "Multi2"}], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert isinstance(result, list) - assert len(result) == 2 - for item in result: - assert item == { - "title": "Test Item", - "size": 100, - "description": "This is a test item", - "sub": {"name": "SubItem1"}, - "multi": [{"name": "Multi1"}, {"name": "Multi2"}], - } - - -def test_item_to_list_filter(): - response = client.post( - "/item-filter", - json={ - "title": "Filtered Item", - "size": 200, - "description": "Test filtering", - "sub": {"name": "SubFiltered"}, - "multi": [], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert isinstance(result, list) - assert len(result) == 2 - for item in result: - assert item == { - "title": "Filtered Item", - "size": 200, - "description": "Test filtering", - "sub": {"name": "SubFiltered"}, - "multi": [], - } - # Verify secret fields are filtered out - assert "secret_data" not in item - assert "internal_id" not in item - assert "internal_id" not in item["sub"] - - -def test_list_to_item(): - response = client.post( - "/item-list", - json=[ - {"title": "First Item", "size": 50, "sub": {"name": "First Sub"}}, - {"title": "Second Item", "size": 75, "sub": {"name": "Second Sub"}}, - ], - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "First Item", - "size": 50, - "description": None, - "sub": {"name": "First Sub"}, - "multi": [], - } - - -def test_list_to_item_empty(): - response = client.post( - "/item-list", - json=[], - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "", - "size": 0, - "description": None, - "sub": {"name": ""}, - "multi": [], - } - - -def test_list_to_item_filter(): - response = client.post( - "/item-list-filter", - json=[ - { - "title": "First Item", - "size": 100, - "sub": {"name": "First Sub"}, - "multi": [{"name": "Multi1"}], - }, - {"title": "Second Item", "size": 200, "sub": {"name": "Second Sub"}}, - ], - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == { - "title": "First Item", - "size": 100, - "description": None, - "sub": {"name": "First Sub"}, - "multi": [{"name": "Multi1"}], - } - # Verify secret fields are filtered out - assert "secret_data" not in result - assert "internal_id" not in result - - -def test_list_to_item_filter_no_data(): - response = client.post("/item-list-filter", json=[]) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "", - "size": 0, - "description": None, - "sub": {"name": ""}, - "multi": [], - } - - -def test_list_to_list(): - input_items = [ - {"title": "Item 1", "size": 10, "sub": {"name": "Sub1"}}, - { - "title": "Item 2", - "size": 20, - "description": "Second item", - "sub": {"name": "Sub2"}, - "multi": [{"name": "M1"}, {"name": "M2"}], - }, - {"title": "Item 3", "size": 30, "sub": {"name": "Sub3"}}, - ] - response = client.post( - "/item-list-to-list", - json=input_items, - ) - assert response.status_code == 200, response.text - result = response.json() - assert isinstance(result, list) - assert len(result) == 3 - assert result[0] == { - "title": "Item 1", - "size": 10, - "description": None, - "sub": {"name": "Sub1"}, - "multi": [], - } - assert result[1] == { - "title": "Item 2", - "size": 20, - "description": "Second item", - "sub": {"name": "Sub2"}, - "multi": [{"name": "M1"}, {"name": "M2"}], - } - assert result[2] == { - "title": "Item 3", - "size": 30, - "description": None, - "sub": {"name": "Sub3"}, - "multi": [], - } - - -def test_list_to_list_filter(): - response = client.post( - "/item-list-to-list-filter", - json=[{"title": "Item 1", "size": 100, "sub": {"name": "Sub1"}}], - ) - assert response.status_code == 200, response.text - result = response.json() - assert isinstance(result, list) - assert len(result) == 2 - for item in result: - assert item == { - "title": "Item 1", - "size": 100, - "description": None, - "sub": {"name": "Sub1"}, - "multi": [], - } - # Verify secret fields are filtered out - assert "secret_data" not in item - assert "internal_id" not in item - - -def test_list_to_list_filter_no_data(): - response = client.post( - "/item-list-to-list-filter", - json=[], - ) - assert response.status_code == 200, response.text - assert response.json() == [] - - -def test_list_validation_error(): - response = client.post( - "/item-list", - json=[ - {"title": "Valid Item", "size": 100, "sub": {"name": "Sub1"}}, - { - "title": "Invalid Item" - # Missing required fields: size and sub - }, - ], - ) - assert response.status_code == 422, response.text - error_detail = response.json()["detail"] - assert len(error_detail) == 2 - assert { - "loc": ["body", 1, "size"], - "msg": "field required", - "type": "value_error.missing", - } in error_detail - assert { - "loc": ["body", 1, "sub"], - "msg": "field required", - "type": "value_error.missing", - } in error_detail - - -def test_list_nested_validation_error(): - response = client.post( - "/item-list", - json=[ - {"title": "Item with bad sub", "size": 100, "sub": {"wrong_field": "value"}} - ], - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", 0, "sub", "name"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_list_type_validation_error(): - response = client.post( - "/item-list", - json=[{"title": "Item", "size": "not_a_number", "sub": {"name": "Sub"}}], - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", 0, "size"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) - - -def test_invalid_list_structure(): - response = client.post( - "/item-list", - json={"title": "Not a list", "size": 100, "sub": {"name": "Sub"}}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid list", - "type": "type_error.list", - } - ] - } - ) - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/item": { - "post": { - "summary": "Handle Item", - "operationId": "handle_item_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle Item Item Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item-filter": { - "post": { - "summary": "Handle Item Filter", - "operationId": "handle_item_filter_item_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle Item Filter Item Filter Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item-list": { - "post": { - "summary": "Handle Item List", - "operationId": "handle_item_list_item_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item-list-filter": { - "post": { - "summary": "Handle Item List Filter", - "operationId": "handle_item_list_filter_item_list_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item-list-to-list": { - "post": { - "summary": "Handle Item List To List", - "operationId": "handle_item_list_to_list_item_list_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle Item List To List Item List To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item-list-to-list-filter": { - "post": { - "summary": "Handle Item List To List Filter", - "operationId": "handle_item_list_to_list_filter_item_list_to_list_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle Item List To List Filter Item List To List Filter Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "title": {"type": "string", "title": "Title"}, - "size": {"type": "integer", "title": "Size"}, - "description": {"type": "string", "title": "Description"}, - "sub": {"$ref": "#/components/schemas/SubItem"}, - "multi": { - "items": {"$ref": "#/components/schemas/SubItem"}, - "type": "array", - "title": "Multi", - "default": [], - }, - }, - "type": "object", - "required": ["title", "size", "sub"], - "title": "Item", - }, - "SubItem": { - "properties": {"name": {"type": "string", "title": "Name"}}, - "type": "object", - "required": ["name"], - "title": "SubItem", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_pydantic_v1_v2_mixed.py b/tests/test_pydantic_v1_v2_mixed.py deleted file mode 100644 index 895835a4c0..0000000000 --- a/tests/test_pydantic_v1_v2_mixed.py +++ /dev/null @@ -1,1408 +0,0 @@ -import sys -import warnings -from typing import Any, Union - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi import FastAPI -from fastapi._compat.v1 import BaseModel -from fastapi.testclient import TestClient -from inline_snapshot import snapshot -from pydantic import BaseModel as NewBaseModel - - -class SubItem(BaseModel): - name: str - - -class Item(BaseModel): - title: str - size: int - description: Union[str, None] = None - sub: SubItem - multi: list[SubItem] = [] - - -class NewSubItem(NewBaseModel): - new_sub_name: str - - -class NewItem(NewBaseModel): - new_title: str - new_size: int - new_description: Union[str, None] = None - new_sub: NewSubItem - new_multi: list[NewSubItem] = [] - - -app = FastAPI() - -with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.post("/v1-to-v2/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", - }, - } - }, - } - ) diff --git a/tests/test_pydantic_v1_v2_multifile/__init__.py b/tests/test_pydantic_v1_v2_multifile/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/test_pydantic_v1_v2_multifile/main.py b/tests/test_pydantic_v1_v2_multifile/main.py deleted file mode 100644 index 4180ec3bf5..0000000000 --- a/tests/test_pydantic_v1_v2_multifile/main.py +++ /dev/null @@ -1,137 +0,0 @@ -import warnings - -from fastapi import FastAPI - -from . import modelsv1, modelsv2, modelsv2b - -app = FastAPI() - -with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.post("/v1-to-v2/item") - def handle_v1_item_to_v2(data: modelsv1.Item) -> modelsv2.Item: - return modelsv2.Item( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=modelsv2.SubItem(new_sub_name=data.sub.name), - new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi], - ) - - @app.post("/v2-to-v1/item") - def handle_v2_item_to_v1(data: modelsv2.Item) -> modelsv1.Item: - return modelsv1.Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=modelsv1.SubItem(name=data.new_sub.new_sub_name), - multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi], - ) - - @app.post("/v1-to-v2/item-to-list") - def handle_v1_item_to_v2_list(data: modelsv1.Item) -> list[modelsv2.Item]: - converted = modelsv2.Item( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=modelsv2.SubItem(new_sub_name=data.sub.name), - new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi], - ) - return [converted, converted] - - @app.post("/v1-to-v2/list-to-list") - def handle_v1_list_to_v2_list(data: list[modelsv1.Item]) -> list[modelsv2.Item]: - result = [] - for item in data: - result.append( - modelsv2.Item( - new_title=item.title, - new_size=item.size, - new_description=item.description, - new_sub=modelsv2.SubItem(new_sub_name=item.sub.name), - new_multi=[ - modelsv2.SubItem(new_sub_name=s.name) for s in item.multi - ], - ) - ) - return result - - @app.post("/v1-to-v2/list-to-item") - def handle_v1_list_to_v2_item(data: list[modelsv1.Item]) -> modelsv2.Item: - if data: - item = data[0] - return modelsv2.Item( - new_title=item.title, - new_size=item.size, - new_description=item.description, - new_sub=modelsv2.SubItem(new_sub_name=item.sub.name), - new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in item.multi], - ) - return modelsv2.Item( - new_title="", new_size=0, new_sub=modelsv2.SubItem(new_sub_name="") - ) - - @app.post("/v2-to-v1/item-to-list") - def handle_v2_item_to_v1_list(data: modelsv2.Item) -> list[modelsv1.Item]: - converted = modelsv1.Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=modelsv1.SubItem(name=data.new_sub.new_sub_name), - multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi], - ) - return [converted, converted] - - @app.post("/v2-to-v1/list-to-list") - def handle_v2_list_to_v1_list(data: list[modelsv2.Item]) -> list[modelsv1.Item]: - result = [] - for item in data: - result.append( - modelsv1.Item( - title=item.new_title, - size=item.new_size, - description=item.new_description, - sub=modelsv1.SubItem(name=item.new_sub.new_sub_name), - multi=[ - modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi - ], - ) - ) - return result - - @app.post("/v2-to-v1/list-to-item") - def handle_v2_list_to_v1_item(data: list[modelsv2.Item]) -> modelsv1.Item: - if data: - item = data[0] - return modelsv1.Item( - title=item.new_title, - size=item.new_size, - description=item.new_description, - sub=modelsv1.SubItem(name=item.new_sub.new_sub_name), - multi=[modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi], - ) - return modelsv1.Item(title="", size=0, sub=modelsv1.SubItem(name="")) - - @app.post("/v2-to-v1/same-name") - def handle_v2_same_name_to_v1( - item1: modelsv2.Item, item2: modelsv2b.Item - ) -> modelsv1.Item: - return modelsv1.Item( - title=item1.new_title, - size=item2.dup_size, - description=item1.new_description, - sub=modelsv1.SubItem(name=item1.new_sub.new_sub_name), - multi=[modelsv1.SubItem(name=s.dup_sub_name) for s in item2.dup_multi], - ) - - @app.post("/v2-to-v1/list-of-items-to-list-of-items") - def handle_v2_items_in_list_to_v1_item_in_list( - data1: list[modelsv2.ItemInList], data2: list[modelsv2b.ItemInList] - ) -> list[modelsv1.ItemInList]: - item1 = data1[0] - item2 = data2[0] - return [ - modelsv1.ItemInList(name1=item1.name2), - modelsv1.ItemInList(name1=item2.dup_name2), - ] diff --git a/tests/test_pydantic_v1_v2_multifile/modelsv1.py b/tests/test_pydantic_v1_v2_multifile/modelsv1.py deleted file mode 100644 index 0cc8de4559..0000000000 --- a/tests/test_pydantic_v1_v2_multifile/modelsv1.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Union - -from fastapi._compat.v1 import BaseModel - - -class SubItem(BaseModel): - name: str - - -class Item(BaseModel): - title: str - size: int - description: Union[str, None] = None - sub: SubItem - multi: list[SubItem] = [] - - -class ItemInList(BaseModel): - name1: str diff --git a/tests/test_pydantic_v1_v2_multifile/modelsv2.py b/tests/test_pydantic_v1_v2_multifile/modelsv2.py deleted file mode 100644 index d80b77e103..0000000000 --- a/tests/test_pydantic_v1_v2_multifile/modelsv2.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Union - -from pydantic import BaseModel - - -class SubItem(BaseModel): - new_sub_name: str - - -class Item(BaseModel): - new_title: str - new_size: int - new_description: Union[str, None] = None - new_sub: SubItem - new_multi: list[SubItem] = [] - - -class ItemInList(BaseModel): - name2: str diff --git a/tests/test_pydantic_v1_v2_multifile/modelsv2b.py b/tests/test_pydantic_v1_v2_multifile/modelsv2b.py deleted file mode 100644 index e992bea2e1..0000000000 --- a/tests/test_pydantic_v1_v2_multifile/modelsv2b.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Union - -from pydantic import BaseModel - - -class SubItem(BaseModel): - dup_sub_name: str - - -class Item(BaseModel): - dup_title: str - dup_size: int - dup_description: Union[str, None] = None - dup_sub: SubItem - dup_multi: list[SubItem] = [] - - -class ItemInList(BaseModel): - dup_name2: str diff --git a/tests/test_pydantic_v1_v2_multifile/test_multifile.py b/tests/test_pydantic_v1_v2_multifile/test_multifile.py deleted file mode 100644 index 32d9019616..0000000000 --- a/tests/test_pydantic_v1_v2_multifile/test_multifile.py +++ /dev/null @@ -1,951 +0,0 @@ -import sys - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi.testclient import TestClient -from inline_snapshot import snapshot - -from .main import app - -client = TestClient(app) - - -def test_v1_to_v2_item(): - response = client.post( - "/v1-to-v2/item", - json={"title": "Test", "size": 10, "sub": {"name": "SubTest"}}, - ) - assert response.status_code == 200 - assert response.json() == { - "new_title": "Test", - "new_size": 10, - "new_description": None, - "new_sub": {"new_sub_name": "SubTest"}, - "new_multi": [], - } - - -def test_v2_to_v1_item(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "NewTest", - "new_size": 20, - "new_sub": {"new_sub_name": "NewSubTest"}, - }, - ) - assert response.status_code == 200 - assert response.json() == { - "title": "NewTest", - "size": 20, - "description": None, - "sub": {"name": "NewSubTest"}, - "multi": [], - } - - -def test_v1_to_v2_item_to_list(): - response = client.post( - "/v1-to-v2/item-to-list", - json={"title": "ListTest", "size": 30, "sub": {"name": "SubListTest"}}, - ) - assert response.status_code == 200 - assert response.json() == [ - { - "new_title": "ListTest", - "new_size": 30, - "new_description": None, - "new_sub": {"new_sub_name": "SubListTest"}, - "new_multi": [], - }, - { - "new_title": "ListTest", - "new_size": 30, - "new_description": None, - "new_sub": {"new_sub_name": "SubListTest"}, - "new_multi": [], - }, - ] - - -def test_v1_to_v2_list_to_list(): - response = client.post( - "/v1-to-v2/list-to-list", - json=[ - {"title": "Item1", "size": 40, "sub": {"name": "Sub1"}}, - {"title": "Item2", "size": 50, "sub": {"name": "Sub2"}}, - ], - ) - assert response.status_code == 200 - assert response.json() == [ - { - "new_title": "Item1", - "new_size": 40, - "new_description": None, - "new_sub": {"new_sub_name": "Sub1"}, - "new_multi": [], - }, - { - "new_title": "Item2", - "new_size": 50, - "new_description": None, - "new_sub": {"new_sub_name": "Sub2"}, - "new_multi": [], - }, - ] - - -def test_v1_to_v2_list_to_item(): - response = client.post( - "/v1-to-v2/list-to-item", - json=[ - {"title": "FirstItem", "size": 60, "sub": {"name": "FirstSub"}}, - {"title": "SecondItem", "size": 70, "sub": {"name": "SecondSub"}}, - ], - ) - assert response.status_code == 200 - assert response.json() == { - "new_title": "FirstItem", - "new_size": 60, - "new_description": None, - "new_sub": {"new_sub_name": "FirstSub"}, - "new_multi": [], - } - - -def test_v2_to_v1_item_to_list(): - response = client.post( - "/v2-to-v1/item-to-list", - json={ - "new_title": "ListNew", - "new_size": 80, - "new_sub": {"new_sub_name": "SubListNew"}, - }, - ) - assert response.status_code == 200 - assert response.json() == [ - { - "title": "ListNew", - "size": 80, - "description": None, - "sub": {"name": "SubListNew"}, - "multi": [], - }, - { - "title": "ListNew", - "size": 80, - "description": None, - "sub": {"name": "SubListNew"}, - "multi": [], - }, - ] - - -def test_v2_to_v1_list_to_list(): - response = client.post( - "/v2-to-v1/list-to-list", - json=[ - { - "new_title": "New1", - "new_size": 90, - "new_sub": {"new_sub_name": "NewSub1"}, - }, - { - "new_title": "New2", - "new_size": 100, - "new_sub": {"new_sub_name": "NewSub2"}, - }, - ], - ) - assert response.status_code == 200 - assert response.json() == [ - { - "title": "New1", - "size": 90, - "description": None, - "sub": {"name": "NewSub1"}, - "multi": [], - }, - { - "title": "New2", - "size": 100, - "description": None, - "sub": {"name": "NewSub2"}, - "multi": [], - }, - ] - - -def test_v2_to_v1_list_to_item(): - response = client.post( - "/v2-to-v1/list-to-item", - json=[ - { - "new_title": "FirstNew", - "new_size": 110, - "new_sub": {"new_sub_name": "FirstNewSub"}, - }, - { - "new_title": "SecondNew", - "new_size": 120, - "new_sub": {"new_sub_name": "SecondNewSub"}, - }, - ], - ) - assert response.status_code == 200 - assert response.json() == { - "title": "FirstNew", - "size": 110, - "description": None, - "sub": {"name": "FirstNewSub"}, - "multi": [], - } - - -def test_v1_to_v2_list_to_item_empty(): - response = client.post("/v1-to-v2/list-to-item", json=[]) - assert response.status_code == 200 - assert response.json() == { - "new_title": "", - "new_size": 0, - "new_description": None, - "new_sub": {"new_sub_name": ""}, - "new_multi": [], - } - - -def test_v2_to_v1_list_to_item_empty(): - response = client.post("/v2-to-v1/list-to-item", json=[]) - assert response.status_code == 200 - assert response.json() == { - "title": "", - "size": 0, - "description": None, - "sub": {"name": ""}, - "multi": [], - } - - -def test_v2_same_name_to_v1(): - response = client.post( - "/v2-to-v1/same-name", - json={ - "item1": { - "new_title": "Title1", - "new_size": 100, - "new_description": "Description1", - "new_sub": {"new_sub_name": "Sub1"}, - "new_multi": [{"new_sub_name": "Multi1"}], - }, - "item2": { - "dup_title": "Title2", - "dup_size": 200, - "dup_description": "Description2", - "dup_sub": {"dup_sub_name": "Sub2"}, - "dup_multi": [ - {"dup_sub_name": "Multi2a"}, - {"dup_sub_name": "Multi2b"}, - ], - }, - }, - ) - assert response.status_code == 200 - assert response.json() == { - "title": "Title1", - "size": 200, - "description": "Description1", - "sub": {"name": "Sub1"}, - "multi": [{"name": "Multi2a"}, {"name": "Multi2b"}], - } - - -def test_v2_items_in_list_to_v1_item_in_list(): - response = client.post( - "/v2-to-v1/list-of-items-to-list-of-items", - json={ - "data1": [{"name2": "Item1"}, {"name2": "Item2"}], - "data2": [{"dup_name2": "Item3"}, {"dup_name2": "Item4"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == [ - {"name1": "Item1"}, - {"name1": "Item3"}, - ] - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/v1-to-v2/item": { - "post": { - "summary": "Handle V1 Item To V2", - "operationId": "handle_v1_item_to_v2_v1_to_v2_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item": { - "post": { - "summary": "Handle V2 Item To V1", - "operationId": "handle_v2_item_to_v1_v2_to_v1_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - }, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/item-to-list": { - "post": { - "summary": "Handle V1 Item To V2 List", - "operationId": "handle_v1_item_to_v2_list_v1_to_v2_item_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - }, - "type": "array", - "title": "Response Handle V1 Item To V2 List V1 To V2 Item To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/list-to-list": { - "post": { - "summary": "Handle V1 List To V2 List", - "operationId": "handle_v1_list_to_v2_list_v1_to_v2_list_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - }, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - }, - "type": "array", - "title": "Response Handle V1 List To V2 List V1 To V2 List To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/list-to-item": { - "post": { - "summary": "Handle V1 List To V2 Item", - "operationId": "handle_v1_list_to_v2_item_v1_to_v2_list_to_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - }, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item-to-list": { - "post": { - "summary": "Handle V2 Item To V1 List", - "operationId": "handle_v2_item_to_v1_list_v2_to_v1_item_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - }, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - }, - "type": "array", - "title": "Response Handle V2 Item To V1 List V2 To V1 Item To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/list-to-list": { - "post": { - "summary": "Handle V2 List To V1 List", - "operationId": "handle_v2_list_to_v1_list_v2_to_v1_list_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - }, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - }, - "type": "array", - "title": "Response Handle V2 List To V1 List V2 To V1 List To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/list-to-item": { - "post": { - "summary": "Handle V2 List To V1 Item", - "operationId": "handle_v2_list_to_v1_item_v2_to_v1_list_to_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - }, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/same-name": { - "post": { - "summary": "Handle V2 Same Name To V1", - "operationId": "handle_v2_same_name_to_v1_v2_to_v1_same_name_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/list-of-items-to-list-of-items": { - "post": { - "summary": "Handle V2 Items In List To V1 Item In List", - "operationId": "handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList" - }, - "type": "array", - "title": "Response Handle V2 Items In List To V1 Item In List V2 To V1 List Of Items To List Of Items Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post": { - "properties": { - "data1": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList" - }, - "type": "array", - "title": "Data1", - }, - "data2": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList" - }, - "type": "array", - "title": "Data2", - }, - }, - "type": "object", - "required": ["data1", "data2"], - "title": "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post", - }, - "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post": { - "properties": { - "item1": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - }, - "item2": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__Item" - }, - }, - "type": "object", - "required": ["item1", "item2"], - "title": "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post", - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [ - {"type": "string"}, - {"type": "integer"}, - ] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv1__Item": { - "properties": { - "title": {"type": "string", "title": "Title"}, - "size": {"type": "integer", "title": "Size"}, - "description": { - "type": "string", - "title": "Description", - }, - "sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem" - }, - "multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem" - }, - "type": "array", - "title": "Multi", - "default": [], - }, - }, - "type": "object", - "required": ["title", "size", "sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList": { - "properties": {"name1": {"type": "string", "title": "Name1"}}, - "type": "object", - "required": ["name1"], - "title": "ItemInList", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem": { - "properties": {"name": {"type": "string", "title": "Name"}}, - "type": "object", - "required": ["name"], - "title": "SubItem", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__Item": { - "properties": { - "new_title": { - "type": "string", - "title": "New Title", - }, - "new_size": { - "type": "integer", - "title": "New Size", - }, - "new_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "New Description", - }, - "new_sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "new_multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "type": "array", - "title": "New Multi", - "default": [], - }, - }, - "type": "object", - "required": ["new_title", "new_size", "new_sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input": { - "properties": { - "new_title": { - "type": "string", - "title": "New Title", - }, - "new_size": { - "type": "integer", - "title": "New Size", - }, - "new_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "New Description", - }, - "new_sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "new_multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "type": "array", - "title": "New Multi", - "default": [], - }, - }, - "type": "object", - "required": ["new_title", "new_size", "new_sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList": { - "properties": {"name2": {"type": "string", "title": "Name2"}}, - "type": "object", - "required": ["name2"], - "title": "ItemInList", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem": { - "properties": { - "new_sub_name": { - "type": "string", - "title": "New Sub Name", - } - }, - "type": "object", - "required": ["new_sub_name"], - "title": "SubItem", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2b__Item": { - "properties": { - "dup_title": { - "type": "string", - "title": "Dup Title", - }, - "dup_size": { - "type": "integer", - "title": "Dup Size", - }, - "dup_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Dup Description", - }, - "dup_sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem" - }, - "dup_multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem" - }, - "type": "array", - "title": "Dup Multi", - "default": [], - }, - }, - "type": "object", - "required": ["dup_title", "dup_size", "dup_sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList": { - "properties": { - "dup_name2": { - "type": "string", - "title": "Dup Name2", - } - }, - "type": "object", - "required": ["dup_name2"], - "title": "ItemInList", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem": { - "properties": { - "dup_sub_name": { - "type": "string", - "title": "Dup Sub Name", - } - }, - "type": "object", - "required": ["dup_sub_name"], - "title": "SubItem", - }, - }, - }, - } - ) diff --git a/tests/test_pydantic_v1_v2_noneable.py b/tests/test_pydantic_v1_v2_noneable.py deleted file mode 100644 index ba98b5653c..0000000000 --- a/tests/test_pydantic_v1_v2_noneable.py +++ /dev/null @@ -1,692 +0,0 @@ -import sys -import warnings -from typing import Any, Union - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi import FastAPI -from fastapi._compat.v1 import BaseModel -from fastapi.testclient import TestClient -from inline_snapshot import snapshot -from pydantic import BaseModel as NewBaseModel - - -class SubItem(BaseModel): - name: str - - -class Item(BaseModel): - title: str - size: int - description: Union[str, None] = None - sub: SubItem - multi: list[SubItem] = [] - - -class NewSubItem(NewBaseModel): - new_sub_name: str - - -class NewItem(NewBaseModel): - new_title: str - new_size: int - new_description: Union[str, None] = None - new_sub: NewSubItem - new_multi: list[NewSubItem] = [] - - -app = FastAPI() - -with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.post("/v1-to-v2/") - def handle_v1_item_to_v2(data: Item) -> Union[NewItem, None]: - if data.size < 0: - return None - return NewItem( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=NewSubItem(new_sub_name=data.sub.name), - new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], - ) - - @app.post("/v1-to-v2/item-filter", response_model=Union[NewItem, None]) - def handle_v1_item_to_v2_filter(data: Item) -> Any: - if data.size < 0: - return None - result = { - "new_title": data.title, - "new_size": data.size, - "new_description": data.description, - "new_sub": { - "new_sub_name": data.sub.name, - "new_sub_secret": "sub_hidden", - }, - "new_multi": [ - {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} - for s in data.multi - ], - "secret": "hidden_v1_to_v2", - } - return result - - @app.post("/v2-to-v1/item") - def handle_v2_item_to_v1(data: NewItem) -> Union[Item, None]: - if data.new_size < 0: - return None - return Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=SubItem(name=data.new_sub.new_sub_name), - multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], - ) - - @app.post("/v2-to-v1/item-filter", response_model=Union[Item, None]) - def handle_v2_item_to_v1_filter(data: NewItem) -> Any: - if data.new_size < 0: - return None - result = { - "title": data.new_title, - "size": data.new_size, - "description": data.new_description, - "sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"}, - "multi": [ - {"name": s.new_sub_name, "sub_secret": "sub_hidden"} - for s in data.new_multi - ], - "secret": "hidden_v2_to_v1", - } - return result - - -client = TestClient(app) - - -def test_v1_to_v2_item_success(): - response = client.post( - "/v1-to-v2/", - json={ - "title": "Old Item", - "size": 100, - "description": "V1 description", - "sub": {"name": "V1 Sub"}, - "multi": [{"name": "M1"}, {"name": "M2"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "new_title": "Old Item", - "new_size": 100, - "new_description": "V1 description", - "new_sub": {"new_sub_name": "V1 Sub"}, - "new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}], - } - - -def test_v1_to_v2_item_returns_none(): - response = client.post( - "/v1-to-v2/", - json={"title": "Invalid Item", "size": -10, "sub": {"name": "Sub"}}, - ) - assert response.status_code == 200, response.text - assert response.json() is None - - -def test_v1_to_v2_item_minimal(): - response = client.post( - "/v1-to-v2/", json={"title": "Minimal", "size": 50, "sub": {"name": "MinSub"}} - ) - assert response.status_code == 200, response.text - assert response.json() == { - "new_title": "Minimal", - "new_size": 50, - "new_description": None, - "new_sub": {"new_sub_name": "MinSub"}, - "new_multi": [], - } - - -def test_v1_to_v2_item_filter_success(): - response = client.post( - "/v1-to-v2/item-filter", - json={ - "title": "Filtered Item", - "size": 50, - "sub": {"name": "Sub"}, - "multi": [{"name": "Multi1"}], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert result["new_title"] == "Filtered Item" - assert result["new_size"] == 50 - assert result["new_sub"]["new_sub_name"] == "Sub" - assert result["new_multi"][0]["new_sub_name"] == "Multi1" - # Verify secret fields are filtered out - assert "secret" not in result - assert "new_sub_secret" not in result["new_sub"] - assert "new_sub_secret" not in result["new_multi"][0] - - -def test_v1_to_v2_item_filter_returns_none(): - response = client.post( - "/v1-to-v2/item-filter", - json={"title": "Invalid", "size": -1, "sub": {"name": "Sub"}}, - ) - assert response.status_code == 200, response.text - assert response.json() is None - - -def test_v2_to_v1_item_success(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "New Item", - "new_size": 200, - "new_description": "V2 description", - "new_sub": {"new_sub_name": "V2 Sub"}, - "new_multi": [{"new_sub_name": "N1"}, {"new_sub_name": "N2"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "New Item", - "size": 200, - "description": "V2 description", - "sub": {"name": "V2 Sub"}, - "multi": [{"name": "N1"}, {"name": "N2"}], - } - - -def test_v2_to_v1_item_returns_none(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "Invalid New", - "new_size": -5, - "new_sub": {"new_sub_name": "NewSub"}, - }, - ) - assert response.status_code == 200, response.text - assert response.json() is None - - -def test_v2_to_v1_item_minimal(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "MinimalNew", - "new_size": 75, - "new_sub": {"new_sub_name": "MinNewSub"}, - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "MinimalNew", - "size": 75, - "description": None, - "sub": {"name": "MinNewSub"}, - "multi": [], - } - - -def test_v2_to_v1_item_filter_success(): - response = client.post( - "/v2-to-v1/item-filter", - json={ - "new_title": "Filtered New", - "new_size": 75, - "new_sub": {"new_sub_name": "NewSub"}, - "new_multi": [], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert result["title"] == "Filtered New" - assert result["size"] == 75 - assert result["sub"]["name"] == "NewSub" - # Verify secret fields are filtered out - assert "secret" not in result - assert "sub_secret" not in result["sub"] - - -def test_v2_to_v1_item_filter_returns_none(): - response = client.post( - "/v2-to-v1/item-filter", - json={ - "new_title": "Invalid Filtered", - "new_size": -100, - "new_sub": {"new_sub_name": "Sub"}, - }, - ) - assert response.status_code == 200, response.text - assert response.json() is None - - -def test_v1_to_v2_validation_error(): - response = client.post("/v1-to-v2/", json={"title": "Missing fields"}) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "size"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "sub"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_v1_to_v2_nested_validation_error(): - response = client.post( - "/v1-to-v2/", - json={"title": "Bad sub", "size": 100, "sub": {"wrong_field": "value"}}, - ) - assert response.status_code == 422, response.text - error_detail = response.json()["detail"] - assert len(error_detail) == 1 - assert error_detail[0]["loc"] == ["body", "sub", "name"] - - -def test_v1_to_v2_type_validation_error(): - response = client.post( - "/v1-to-v2/", - json={"title": "Bad type", "size": "not_a_number", "sub": {"name": "Sub"}}, - ) - assert response.status_code == 422, response.text - error_detail = response.json()["detail"] - assert len(error_detail) == 1 - assert error_detail[0]["loc"] == ["body", "size"] - - -def test_v2_to_v1_validation_error(): - response = client.post("/v2-to-v1/item", json={"new_title": "Missing fields"}) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "new_size"], - "msg": "Field required", - "input": {"new_title": "Missing fields"}, - }, - { - "type": "missing", - "loc": ["body", "new_sub"], - "msg": "Field required", - "input": {"new_title": "Missing fields"}, - }, - ] - } - ) - - -def test_v2_to_v1_nested_validation_error(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "Bad sub", - "new_size": 200, - "new_sub": {"wrong_field": "value"}, - }, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "new_sub", "new_sub_name"], - "msg": "Field required", - "input": {"wrong_field": "value"}, - } - ] - } - ) - - -def test_v2_to_v1_type_validation_error(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "Bad type", - "new_size": "not_a_number", - "new_sub": {"new_sub_name": "Sub"}, - }, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["body", "new_size"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "not_a_number", - } - ] - } - ) - - -def test_v1_to_v2_with_multi_items(): - response = client.post( - "/v1-to-v2/", - json={ - "title": "Complex Item", - "size": 300, - "description": "Item with multiple sub-items", - "sub": {"name": "Main Sub"}, - "multi": [{"name": "Sub1"}, {"name": "Sub2"}, {"name": "Sub3"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "new_title": "Complex Item", - "new_size": 300, - "new_description": "Item with multiple sub-items", - "new_sub": {"new_sub_name": "Main Sub"}, - "new_multi": [ - {"new_sub_name": "Sub1"}, - {"new_sub_name": "Sub2"}, - {"new_sub_name": "Sub3"}, - ], - } - ) - - -def test_v2_to_v1_with_multi_items(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "Complex New Item", - "new_size": 400, - "new_description": "New item with multiple sub-items", - "new_sub": {"new_sub_name": "Main New Sub"}, - "new_multi": [{"new_sub_name": "NewSub1"}, {"new_sub_name": "NewSub2"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "title": "Complex New Item", - "size": 400, - "description": "New item with multiple sub-items", - "sub": {"name": "Main New Sub"}, - "multi": [{"name": "NewSub1"}, {"name": "NewSub2"}], - } - ) - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/v1-to-v2/": { - "post": { - "summary": "Handle V1 Item To V2", - "operationId": "handle_v1_item_to_v2_v1_to_v2__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/NewItem" - }, - {"type": "null"}, - ], - "title": "Response Handle V1 Item To V2 V1 To V2 Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/item-filter": { - "post": { - "summary": "Handle V1 Item To V2 Filter", - "operationId": "handle_v1_item_to_v2_filter_v1_to_v2_item_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/NewItem" - }, - {"type": "null"}, - ], - "title": "Response Handle V1 Item To V2 Filter V1 To V2 Item Filter Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item": { - "post": { - "summary": "Handle V2 Item To V1", - "operationId": "handle_v2_item_to_v1_v2_to_v1_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/NewItem"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item-filter": { - "post": { - "summary": "Handle V2 Item To V1 Filter", - "operationId": "handle_v2_item_to_v1_filter_v2_to_v1_item_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/NewItem"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "title": {"type": "string", "title": "Title"}, - "size": {"type": "integer", "title": "Size"}, - "description": {"type": "string", "title": "Description"}, - "sub": {"$ref": "#/components/schemas/SubItem"}, - "multi": { - "items": {"$ref": "#/components/schemas/SubItem"}, - "type": "array", - "title": "Multi", - "default": [], - }, - }, - "type": "object", - "required": ["title", "size", "sub"], - "title": "Item", - }, - "NewItem": { - "properties": { - "new_title": {"type": "string", "title": "New Title"}, - "new_size": {"type": "integer", "title": "New Size"}, - "new_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "New Description", - }, - "new_sub": {"$ref": "#/components/schemas/NewSubItem"}, - "new_multi": { - "items": {"$ref": "#/components/schemas/NewSubItem"}, - "type": "array", - "title": "New Multi", - "default": [], - }, - }, - "type": "object", - "required": ["new_title", "new_size", "new_sub"], - "title": "NewItem", - }, - "NewSubItem": { - "properties": { - "new_sub_name": {"type": "string", "title": "New Sub Name"} - }, - "type": "object", - "required": ["new_sub_name"], - "title": "NewSubItem", - }, - "SubItem": { - "properties": {"name": {"type": "string", "title": "Name"}}, - "type": "object", - "required": ["name"], - "title": "SubItem", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_read_with_orm_mode.py b/tests/test_read_with_orm_mode.py index a195634b8a..cd7389252a 100644 --- a/tests/test_read_with_orm_mode.py +++ b/tests/test_read_with_orm_mode.py @@ -1,12 +1,9 @@ -import warnings from typing import Any from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, ConfigDict -from .utils import needs_pydanticv1 - def test_read_with_orm_mode() -> None: class PersonBase(BaseModel): @@ -44,50 +41,3 @@ def test_read_with_orm_mode() -> None: assert data["name"] == person_data["name"] assert data["lastname"] == person_data["lastname"] assert data["full_name"] == person_data["name"] + " " + person_data["lastname"] - - -@needs_pydanticv1 -def test_read_with_orm_mode_pv1() -> None: - from pydantic import v1 - - class PersonBase(v1.BaseModel): - name: str - lastname: str - - class Person(PersonBase): - @property - def full_name(self) -> str: - return f"{self.name} {self.lastname}" - - class Config: - orm_mode = True - read_with_orm_mode = True - - class PersonCreate(PersonBase): - pass - - class PersonRead(PersonBase): - full_name: str - - class Config: - orm_mode = True - - app = FastAPI() - - with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.post("/people/", response_model=PersonRead) - def create_person(person: PersonCreate) -> Any: - db_person = Person.from_orm(person) - return db_person - - client = TestClient(app) - - person_data = {"name": "Dive", "lastname": "Wilson"} - response = client.post("/people/", json=person_data) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == person_data["name"] - assert data["lastname"] == person_data["lastname"] - assert data["full_name"] == person_data["name"] + " " + person_data["lastname"] diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py index 9e527d6a01..58fba89f1a 100644 --- a/tests/test_response_model_as_return_annotation.py +++ b/tests/test_response_model_as_return_annotation.py @@ -1,4 +1,3 @@ -import warnings from typing import Union import pytest @@ -8,8 +7,6 @@ from fastapi.responses import JSONResponse, Response from fastapi.testclient import TestClient from pydantic import BaseModel -from tests.utils import needs_pydanticv1 - class BaseUser(BaseModel): name: str @@ -512,29 +509,6 @@ def test_invalid_response_model_field(): assert "parameter response_model=None" in e.value.args[0] -# TODO: remove when dropping Pydantic v1 support -@needs_pydanticv1 -def test_invalid_response_model_field_pv1(): - from fastapi._compat import v1 - - app = FastAPI() - - class Model(v1.BaseModel): - foo: str - - with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - with pytest.raises(FastAPIError) as e: - - @app.get("/") - def read_root() -> Union[Response, Model, None]: - return Response(content="Foo") # pragma: no cover - - assert "valid Pydantic field type" in e.value.args[0] - assert "parameter response_model=None" in e.value.args[0] - - def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py deleted file mode 100644 index 62b67a98c1..0000000000 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py +++ /dev/null @@ -1,115 +0,0 @@ -import importlib - -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_pydanticv1 - - -@pytest.fixture( - name="client", - params=[ - pytest.param("tutorial007_pv1_py39"), - ], -) -def get_client(request: pytest.FixtureRequest): - mod = importlib.import_module( - f"docs_src.path_operation_advanced_configuration.{request.param}" - ) - - client = TestClient(mod.app) - return client - - -@needs_pydanticv1 -def test_post(client: TestClient): - yaml_data = """ - name: Deadpoolio - tags: - - x-force - - x-men - - x-avengers - """ - response = client.post("/items/", content=yaml_data) - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Deadpoolio", - "tags": ["x-force", "x-men", "x-avengers"], - } - - -@needs_pydanticv1 -def test_post_broken_yaml(client: TestClient): - yaml_data = """ - name: Deadpoolio - tags: - x - x-force - x - x-men - x - x-avengers - """ - response = client.post("/items/", content=yaml_data) - assert response.status_code == 422, response.text - assert response.json() == {"detail": "Invalid YAML"} - - -@needs_pydanticv1 -def test_post_invalid(client: TestClient): - yaml_data = """ - name: Deadpoolio - tags: - - x-force - - x-men - - x-avengers - - sneaky: object - """ - response = client.post("/items/", content=yaml_data) - assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - {"loc": ["tags", 3], "msg": "str type expected", "type": "type_error.str"} - ] - } - - -@needs_pydanticv1 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/x-yaml": { - "schema": { - "title": "Item", - "required": ["name", "tags"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - }, - }, - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, - } diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/__init__.py b/tests/test_tutorial/test_pydantic_v1_in_v2/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py deleted file mode 100644 index 4090eba012..0000000000 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py +++ /dev/null @@ -1,31 +0,0 @@ -import sys -from typing import Any - -import pytest - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - - -import importlib - -from ...utils import needs_py310 - - -@pytest.fixture( - name="mod", - params=[ - "tutorial001_an_py39", - pytest.param("tutorial001_an_py310", marks=needs_py310), - ], -) -def get_mod(request: pytest.FixtureRequest): - mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") - return mod - - -def test_model(mod: Any): - item = mod.Item(name="Foo", size=3.4) - assert item.dict() == {"name": "Foo", "description": None, "size": 3.4} diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py deleted file mode 100644 index 9d1baf8530..0000000000 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py +++ /dev/null @@ -1,143 +0,0 @@ -import sys -import warnings - -import pytest -from fastapi.exceptions import FastAPIDeprecationWarning -from inline_snapshot import snapshot - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - - -import importlib - -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture( - name="client", - params=[ - "tutorial002_an_py39", - pytest.param("tutorial002_an_py310", marks=needs_py310), - ], -) -def get_client(request: pytest.FixtureRequest): - with warnings.catch_warnings(record=True): - warnings.filterwarnings( - "ignore", - message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=FastAPIDeprecationWarning, - ) - mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") - - c = TestClient(mod.app) - return c - - -def test_call(client: TestClient): - response = client.post("/items/", json={"name": "Foo", "size": 3.4}) - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Foo", - "description": None, - "size": 3.4, - } - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": {"type": "string", "title": "Description"}, - "size": {"type": "number", "title": "Size"}, - }, - "type": "object", - "required": ["name", "size"], - "title": "Item", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py deleted file mode 100644 index 23b236888d..0000000000 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py +++ /dev/null @@ -1,158 +0,0 @@ -import sys -import warnings - -import pytest -from fastapi.exceptions import FastAPIDeprecationWarning -from inline_snapshot import snapshot - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - - -import importlib - -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture( - name="client", - params=[ - "tutorial003_an_py39", - pytest.param("tutorial003_an_py310", marks=needs_py310), - ], -) -def get_client(request: pytest.FixtureRequest): - with warnings.catch_warnings(record=True): - warnings.filterwarnings( - "ignore", - message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=FastAPIDeprecationWarning, - ) - mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") - - c = TestClient(mod.app) - return c - - -def test_call(client: TestClient): - response = client.post("/items/", json={"name": "Foo", "size": 3.4}) - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Foo", - "description": None, - "size": 3.4, - } - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ItemV2" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": {"type": "string", "title": "Description"}, - "size": {"type": "number", "title": "Size"}, - }, - "type": "object", - "required": ["name", "size"], - "title": "Item", - }, - "ItemV2": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - "size": {"type": "number", "title": "Size"}, - }, - "type": "object", - "required": ["name", "size"], - "title": "ItemV2", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py deleted file mode 100644 index 61c0f63571..0000000000 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py +++ /dev/null @@ -1,156 +0,0 @@ -import sys -import warnings - -import pytest -from fastapi.exceptions import FastAPIDeprecationWarning -from inline_snapshot import snapshot - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - - -import importlib - -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture( - name="client", - params=[ - pytest.param("tutorial004_an_py39"), - pytest.param("tutorial004_an_py310", marks=needs_py310), - ], -) -def get_client(request: pytest.FixtureRequest): - with warnings.catch_warnings(record=True): - warnings.filterwarnings( - "ignore", - message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=FastAPIDeprecationWarning, - ) - mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") - - c = TestClient(mod.app) - return c - - -def test_call(client: TestClient): - response = client.post("/items/", json={"item": {"name": "Foo", "size": 3.4}}) - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Foo", - "description": None, - "size": 3.4, - } - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_item_items__post" - } - ], - "title": "Body", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "Body_create_item_items__post": { - "properties": { - "item": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - } - }, - "type": "object", - "required": ["item"], - "title": "Body_create_item_items__post", - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": {"type": "string", "title": "Description"}, - "size": {"type": "number", "title": "Size"}, - }, - "type": "object", - "required": ["name", "size"], - "title": "Item", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py deleted file mode 100644 index 50be458962..0000000000 --- a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py +++ /dev/null @@ -1,128 +0,0 @@ -import importlib -import warnings - -import pytest -from fastapi.exceptions import FastAPIDeprecationWarning -from fastapi.testclient import TestClient - -from ...utils import needs_pydanticv1 - - -@pytest.fixture( - name="client", - params=[ - "tutorial002_pv1_py39", - "tutorial002_pv1_an_py39", - ], -) -def get_client(request: pytest.FixtureRequest): - with warnings.catch_warnings(record=True): - warnings.filterwarnings( - "ignore", - message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=FastAPIDeprecationWarning, - ) - mod = importlib.import_module(f"docs_src.request_form_models.{request.param}") - - client = TestClient(mod.app) - return client - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form(client: TestClient): - response = client.post("/login/", data={"username": "Foo", "password": "secret"}) - assert response.status_code == 200 - assert response.json() == {"username": "Foo", "password": "secret"} - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_extra_form(client: TestClient): - response = client.post( - "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} - ) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.extra", - "loc": ["body", "extra"], - "msg": "extra fields not permitted", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form_no_password(client: TestClient): - response = client.post("/login/", data={"username": "Foo"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form_no_username(client: TestClient): - response = client.post("/login/", data={"password": "secret"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form_no_data(client: TestClient): - response = client.post("/login/") - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - }, - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - }, - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_json(client: TestClient): - response = client.post("/login/", json={"username": "Foo", "password": "secret"}) - assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - }, - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - }, - ] - } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py deleted file mode 100644 index 83c7176567..0000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py +++ /dev/null @@ -1,152 +0,0 @@ -import importlib -import warnings - -import pytest -from fastapi.exceptions import FastAPIDeprecationWarning -from fastapi.testclient import TestClient -from inline_snapshot import snapshot - -from ...utils import needs_py310, needs_pydanticv1 - - -@pytest.fixture( - name="client", - params=[ - pytest.param("tutorial001_pv1_py39"), - pytest.param("tutorial001_pv1_py310", marks=needs_py310), - ], -) -def get_client(request: pytest.FixtureRequest): - with warnings.catch_warnings(record=True): - warnings.filterwarnings( - "ignore", - message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=FastAPIDeprecationWarning, - ) - mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}") - - client = TestClient(mod.app) - return client - - -@needs_pydanticv1 -def test_post_body_example(client: TestClient): - response = client.put( - "/items/5", - json={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - ) - assert response.status_code == 200 - - -@needs_pydanticv1 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"type": "integer", "title": "Item Id"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Item", - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": {"type": "string", "title": "Description"}, - "price": {"type": "number", "title": "Price"}, - "tax": {"type": "number", "title": "Tax"}, - }, - "type": "object", - "required": ["name", "price"], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - } - ], - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_tutorial/test_settings/test_app03.py b/tests/test_tutorial/test_settings/test_app03.py index 06e82398d1..72de497967 100644 --- a/tests/test_tutorial/test_settings/test_app03.py +++ b/tests/test_tutorial/test_settings/test_app03.py @@ -5,8 +5,6 @@ import pytest from fastapi.testclient import TestClient from pytest import MonkeyPatch -from ...utils import needs_pydanticv1 - @pytest.fixture( name="mod_path", @@ -34,16 +32,6 @@ def test_settings(main_mod: ModuleType, monkeypatch: MonkeyPatch): 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) diff --git a/tests/test_tutorial/test_settings/test_tutorial001.py b/tests/test_tutorial/test_settings/test_tutorial001.py index 6a08096989..f4576a0d21 100644 --- a/tests/test_tutorial/test_settings/test_tutorial001.py +++ b/tests/test_tutorial/test_settings/test_tutorial001.py @@ -4,16 +4,8 @@ import pytest 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}") diff --git a/tests/utils.py b/tests/utils.py index b896d4527f..efa0bfd52b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -10,8 +10,6 @@ needs_py_lt_314 = pytest.mark.skipif( 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."""