Browse Source

🔥 Remove conditionals checking if Pydantic v1 was installed, first pass

pull/14575/head
Sebastián Ramírez 7 months ago
parent
commit
00c8b73493
  1. 187
      fastapi/_compat/main.py
  2. 147
      fastapi/_compat/v1.py
  3. 6
      fastapi/dependencies/utils.py
  4. 17
      fastapi/openapi/models.py
  5. 71
      fastapi/params.py
  6. 4
      fastapi/routing.py
  7. 14
      fastapi/utils.py
  8. 8
      tests/test_additional_properties_bool.py
  9. 6
      tests/test_ambiguous_params.py
  10. 30
      tests/test_custom_schema_fields.py
  11. 8
      tests/test_forms_single_model.py
  12. 33
      tests/test_jsonable_encoder.py
  13. 37
      tests/test_openapi_separate_input_output_schemas.py
  14. 8
      tests/test_query_cookie_header_model_extra_params.py
  15. 10
      tests/test_request_param_model_by_alias.py
  16. 27
      tests/test_response_by_alias.py
  17. 12
      tests/test_schema_extra_examples.py
  18. 6
      tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py
  19. 5
      tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py
  20. 4
      tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py
  21. 4
      tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py
  22. 11
      tests/test_validate_response_recursive/app.py

187
fastapi/_compat/main.py

@ -6,43 +6,26 @@ from typing import (
) )
from fastapi._compat import may_v1 from fastapi._compat import may_v1
from fastapi._compat.shared import PYDANTIC_V2, lenient_issubclass from fastapi._compat.shared import lenient_issubclass
from fastapi.types import ModelNameMap from fastapi.types import ModelNameMap
from pydantic import BaseModel from pydantic import BaseModel
from typing_extensions import Literal from typing_extensions import Literal
from . import v2
from .model_field import ModelField from .model_field import ModelField
from .v2 import BaseConfig as BaseConfig
if PYDANTIC_V2: from .v2 import FieldInfo as FieldInfo
from .v2 import BaseConfig as BaseConfig from .v2 import PydanticSchemaGenerationError as PydanticSchemaGenerationError
from .v2 import FieldInfo as FieldInfo from .v2 import RequiredParam as RequiredParam
from .v2 import PydanticSchemaGenerationError as PydanticSchemaGenerationError from .v2 import Undefined as Undefined
from .v2 import RequiredParam as RequiredParam from .v2 import UndefinedType as UndefinedType
from .v2 import Undefined as Undefined from .v2 import Url as Url
from .v2 import UndefinedType as UndefinedType from .v2 import Validator as Validator
from .v2 import Url as Url from .v2 import evaluate_forwardref as evaluate_forwardref
from .v2 import Validator as Validator from .v2 import get_missing_field_error as get_missing_field_error
from .v2 import evaluate_forwardref as evaluate_forwardref from .v2 import (
from .v2 import get_missing_field_error as get_missing_field_error with_info_plain_validator_function as with_info_plain_validator_function,
from .v2 import ( )
with_info_plain_validator_function as with_info_plain_validator_function,
)
else:
from .v1 import BaseConfig as BaseConfig # type: ignore[assignment]
from .v1 import FieldInfo as FieldInfo
from .v1 import ( # type: ignore[assignment]
PydanticSchemaGenerationError as PydanticSchemaGenerationError,
)
from .v1 import RequiredParam as RequiredParam
from .v1 import Undefined as Undefined
from .v1 import UndefinedType as UndefinedType
from .v1 import Url as Url # type: ignore[assignment]
from .v1 import Validator as Validator
from .v1 import evaluate_forwardref as evaluate_forwardref
from .v1 import get_missing_field_error as get_missing_field_error
from .v1 import ( # type: ignore[assignment]
with_info_plain_validator_function as with_info_plain_validator_function,
)
@lru_cache @lru_cache
@ -60,11 +43,8 @@ def get_cached_model_fields(model: type[BaseModel]) -> list[ModelField]:
def _is_undefined(value: object) -> bool: def _is_undefined(value: object) -> bool:
if isinstance(value, may_v1.UndefinedType): if isinstance(value, may_v1.UndefinedType):
return True return True
elif PYDANTIC_V2:
from . import v2
return isinstance(value, v2.UndefinedType) return isinstance(value, v2.UndefinedType)
return False
def _get_model_config(model: BaseModel) -> Any: def _get_model_config(model: BaseModel) -> Any:
@ -72,10 +52,8 @@ def _get_model_config(model: BaseModel) -> Any:
from fastapi._compat import v1 from fastapi._compat import v1
return v1._get_model_config(model) return v1._get_model_config(model)
elif PYDANTIC_V2:
from . import v2
return v2._get_model_config(model) return v2._get_model_config(model)
def _model_dump( def _model_dump(
@ -85,20 +63,15 @@ def _model_dump(
from fastapi._compat import v1 from fastapi._compat import v1
return v1._model_dump(model, mode=mode, **kwargs) return v1._model_dump(model, mode=mode, **kwargs)
elif PYDANTIC_V2:
from . import v2
return v2._model_dump(model, mode=mode, **kwargs) return v2._model_dump(model, mode=mode, **kwargs)
def _is_error_wrapper(exc: Exception) -> bool: def _is_error_wrapper(exc: Exception) -> bool:
if isinstance(exc, may_v1.ErrorWrapper): if isinstance(exc, may_v1.ErrorWrapper):
return True return True
elif PYDANTIC_V2:
from . import v2
return isinstance(exc, v2.ErrorWrapper) return isinstance(exc, v2.ErrorWrapper)
return False
def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
@ -106,11 +79,8 @@ def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
from fastapi._compat import v1 from fastapi._compat import v1
return v1.copy_field_info(field_info=field_info, annotation=annotation) return v1.copy_field_info(field_info=field_info, annotation=annotation)
else:
assert PYDANTIC_V2
from . import v2
return v2.copy_field_info(field_info=field_info, annotation=annotation) return v2.copy_field_info(field_info=field_info, annotation=annotation)
def create_body_model( def create_body_model(
@ -120,11 +90,8 @@ def create_body_model(
from fastapi._compat import v1 from fastapi._compat import v1
return v1.create_body_model(fields=fields, model_name=model_name) return v1.create_body_model(fields=fields, model_name=model_name)
else:
assert PYDANTIC_V2
from . import v2
return v2.create_body_model(fields=fields, model_name=model_name) # type: ignore[arg-type] return v2.create_body_model(fields=fields, model_name=model_name) # type: ignore[arg-type]
def get_annotation_from_field_info( def get_annotation_from_field_info(
@ -136,13 +103,10 @@ def get_annotation_from_field_info(
return v1.get_annotation_from_field_info( return v1.get_annotation_from_field_info(
annotation=annotation, field_info=field_info, field_name=field_name annotation=annotation, field_info=field_info, field_name=field_name
) )
else:
assert PYDANTIC_V2
from . import v2
return v2.get_annotation_from_field_info( return v2.get_annotation_from_field_info(
annotation=annotation, field_info=field_info, field_name=field_name annotation=annotation, field_info=field_info, field_name=field_name
) )
def is_bytes_field(field: ModelField) -> bool: def is_bytes_field(field: ModelField) -> bool:
@ -150,11 +114,8 @@ def is_bytes_field(field: ModelField) -> bool:
from fastapi._compat import v1 from fastapi._compat import v1
return v1.is_bytes_field(field) return v1.is_bytes_field(field)
else:
assert PYDANTIC_V2
from . import v2
return v2.is_bytes_field(field) # type: ignore[arg-type] return v2.is_bytes_field(field) # type: ignore[arg-type]
def is_bytes_sequence_field(field: ModelField) -> bool: def is_bytes_sequence_field(field: ModelField) -> bool:
@ -162,11 +123,8 @@ def is_bytes_sequence_field(field: ModelField) -> bool:
from fastapi._compat import v1 from fastapi._compat import v1
return v1.is_bytes_sequence_field(field) return v1.is_bytes_sequence_field(field)
else:
assert PYDANTIC_V2
from . import v2
return v2.is_bytes_sequence_field(field) # type: ignore[arg-type] return v2.is_bytes_sequence_field(field) # type: ignore[arg-type]
def is_scalar_field(field: ModelField) -> bool: def is_scalar_field(field: ModelField) -> bool:
@ -174,11 +132,8 @@ def is_scalar_field(field: ModelField) -> bool:
from fastapi._compat import v1 from fastapi._compat import v1
return v1.is_scalar_field(field) return v1.is_scalar_field(field)
else:
assert PYDANTIC_V2
from . import v2
return v2.is_scalar_field(field) # type: ignore[arg-type] return v2.is_scalar_field(field) # type: ignore[arg-type]
def is_scalar_sequence_field(field: ModelField) -> bool: def is_scalar_sequence_field(field: ModelField) -> bool:
@ -186,11 +141,8 @@ def is_scalar_sequence_field(field: ModelField) -> bool:
from fastapi._compat import v1 from fastapi._compat import v1
return v1.is_scalar_sequence_field(field) return v1.is_scalar_sequence_field(field)
else:
assert PYDANTIC_V2
from . import v2
return v2.is_scalar_sequence_field(field) # type: ignore[arg-type] return v2.is_scalar_sequence_field(field) # type: ignore[arg-type]
def is_sequence_field(field: ModelField) -> bool: def is_sequence_field(field: ModelField) -> bool:
@ -198,11 +150,8 @@ def is_sequence_field(field: ModelField) -> bool:
from fastapi._compat import v1 from fastapi._compat import v1
return v1.is_sequence_field(field) return v1.is_sequence_field(field)
else:
assert PYDANTIC_V2
from . import v2
return v2.is_sequence_field(field) # type: ignore[arg-type] return v2.is_sequence_field(field) # type: ignore[arg-type]
def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
@ -210,11 +159,8 @@ def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
from fastapi._compat import v1 from fastapi._compat import v1
return v1.serialize_sequence_value(field=field, value=value) return v1.serialize_sequence_value(field=field, value=value)
else:
assert PYDANTIC_V2
from . import v2
return v2.serialize_sequence_value(field=field, value=value) # type: ignore[arg-type] return v2.serialize_sequence_value(field=field, value=value) # type: ignore[arg-type]
def _model_rebuild(model: type[BaseModel]) -> None: def _model_rebuild(model: type[BaseModel]) -> None:
@ -222,9 +168,7 @@ def _model_rebuild(model: type[BaseModel]) -> None:
from fastapi._compat import v1 from fastapi._compat import v1
v1._model_rebuild(model) v1._model_rebuild(model)
elif PYDANTIC_V2: else:
from . import v2
v2._model_rebuild(model) v2._model_rebuild(model)
@ -241,22 +185,12 @@ def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap:
all_flat_models = v1_flat_models all_flat_models = v1_flat_models
else: else:
all_flat_models = set() all_flat_models = set()
if PYDANTIC_V2:
from . import v2
v2_model_fields = [ v2_model_fields = [field for field in fields if isinstance(field, v2.ModelField)]
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)
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)
model_name_map = v2.get_model_name_map(all_flat_models)
return model_name_map
from fastapi._compat import v1
model_name_map = v1.get_model_name_map(all_flat_models) model_name_map = v2.get_model_name_map(all_flat_models)
return model_name_map return model_name_map
@ -279,25 +213,19 @@ def get_definitions(
model_name_map=model_name_map, model_name_map=model_name_map,
separate_input_output_schemas=separate_input_output_schemas, separate_input_output_schemas=separate_input_output_schemas,
) )
if not PYDANTIC_V2:
return v1_field_maps, v1_definitions v2_fields = [field for field in fields if isinstance(field, v2.ModelField)]
else: v2_field_maps, v2_definitions = v2.get_definitions(
from . import v2 fields=v2_fields,
model_name_map=model_name_map,
v2_fields = [field for field in fields if isinstance(field, v2.ModelField)] separate_input_output_schemas=separate_input_output_schemas,
v2_field_maps, v2_definitions = v2.get_definitions( )
fields=v2_fields, all_definitions = {**v1_definitions, **v2_definitions}
model_name_map=model_name_map, all_field_maps = {**v1_field_maps, **v2_field_maps}
separate_input_output_schemas=separate_input_output_schemas, return all_field_maps, all_definitions
)
all_definitions = {**v1_definitions, **v2_definitions}
all_field_maps = {**v1_field_maps, **v2_field_maps}
return all_field_maps, all_definitions
# Pydantic v1 is not supported since Python 3.14 # Pydantic v1 is not supported since Python 3.14
else: else:
from . import v2
v2_fields = [field for field in fields if isinstance(field, v2.ModelField)] v2_fields = [field for field in fields if isinstance(field, v2.ModelField)]
v2_field_maps, v2_definitions = v2.get_definitions( v2_field_maps, v2_definitions = v2.get_definitions(
fields=v2_fields, fields=v2_fields,
@ -326,33 +254,24 @@ def get_schema_from_model_field(
field_mapping=field_mapping, field_mapping=field_mapping,
separate_input_output_schemas=separate_input_output_schemas, separate_input_output_schemas=separate_input_output_schemas,
) )
else:
assert PYDANTIC_V2
from . import v2
return v2.get_schema_from_model_field( return v2.get_schema_from_model_field(
field=field, # type: ignore[arg-type] field=field, # type: ignore[arg-type]
model_name_map=model_name_map, model_name_map=model_name_map,
field_mapping=field_mapping, # type: ignore[arg-type] field_mapping=field_mapping, # type: ignore[arg-type]
separate_input_output_schemas=separate_input_output_schemas, separate_input_output_schemas=separate_input_output_schemas,
) )
def _is_model_field(value: Any) -> bool: def _is_model_field(value: Any) -> bool:
if isinstance(value, may_v1.ModelField): if isinstance(value, may_v1.ModelField):
return True return True
elif PYDANTIC_V2:
from . import v2
return isinstance(value, v2.ModelField) return isinstance(value, v2.ModelField)
return False
def _is_model_class(value: Any) -> bool: def _is_model_class(value: Any) -> bool:
if lenient_issubclass(value, may_v1.BaseModel): if lenient_issubclass(value, may_v1.BaseModel):
return True return True
elif PYDANTIC_V2:
from . import v2
return lenient_issubclass(value, v2.BaseModel) # type: ignore[attr-defined] return lenient_issubclass(value, v2.BaseModel) # type: ignore[attr-defined]
return False

147
fastapi/_compat/v1.py

@ -11,6 +11,56 @@ from typing import (
from fastapi._compat import shared from fastapi._compat import shared
from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX
from fastapi.types import ModelNameMap from fastapi.types import ModelNameMap
from pydantic.v1 import BaseConfig as BaseConfig # type: ignore[assignment]
from pydantic.v1 import BaseModel as BaseModel # type: ignore[assignment]
from pydantic.v1 import ( # type: ignore[assignment]
ValidationError as ValidationError,
)
from pydantic.v1 import create_model as create_model # type: ignore[no-redef]
from pydantic.v1.class_validators import Validator as Validator
from pydantic.v1.color import Color as Color # type: ignore[assignment]
from pydantic.v1.error_wrappers import ErrorWrapper as ErrorWrapper
from pydantic.v1.errors import MissingError
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 # type: ignore[assignment]
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 ( # type: ignore[assignment]
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 ( # type: ignore[assignment]
SecretBytes as SecretBytes,
)
from pydantic.v1.types import ( # type: ignore[assignment]
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 pydantic.version import VERSION as PYDANTIC_VERSION
from typing_extensions import Literal from typing_extensions import Literal
@ -20,103 +70,6 @@ PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2
# shadowing typing.Required. # shadowing typing.Required.
RequiredParam: Any = Ellipsis RequiredParam: Any = Ellipsis
if not PYDANTIC_V2:
from pydantic import BaseConfig as BaseConfig
from pydantic import BaseModel as BaseModel
from pydantic import ValidationError as ValidationError
from pydantic import create_model as create_model
from pydantic.class_validators import Validator as Validator
from pydantic.color import Color as Color
from pydantic.error_wrappers import ErrorWrapper as ErrorWrapper
from pydantic.errors import MissingError
from pydantic.fields import ( # type: ignore[attr-defined]
SHAPE_FROZENSET,
SHAPE_LIST,
SHAPE_SEQUENCE,
SHAPE_SET,
SHAPE_SINGLETON,
SHAPE_TUPLE,
SHAPE_TUPLE_ELLIPSIS,
)
from pydantic.fields import FieldInfo as FieldInfo
from pydantic.fields import ModelField as ModelField # type: ignore[attr-defined]
from pydantic.fields import Undefined as Undefined # type: ignore[attr-defined]
from pydantic.fields import ( # type: ignore[attr-defined]
UndefinedType as UndefinedType,
)
from pydantic.networks import AnyUrl as AnyUrl
from pydantic.networks import NameEmail as NameEmail
from pydantic.schema import TypeModelSet as TypeModelSet
from pydantic.schema import (
field_schema,
model_process_schema,
)
from pydantic.schema import (
get_annotation_from_field_info as get_annotation_from_field_info,
)
from pydantic.schema import get_flat_models_from_field as get_flat_models_from_field
from pydantic.schema import (
get_flat_models_from_fields as get_flat_models_from_fields,
)
from pydantic.schema import get_model_name_map as get_model_name_map
from pydantic.types import SecretBytes as SecretBytes
from pydantic.types import SecretStr as SecretStr
from pydantic.typing import evaluate_forwardref as evaluate_forwardref
from pydantic.utils import lenient_issubclass as lenient_issubclass
else:
from pydantic.v1 import BaseConfig as BaseConfig # type: ignore[assignment]
from pydantic.v1 import BaseModel as BaseModel # type: ignore[assignment]
from pydantic.v1 import ( # type: ignore[assignment]
ValidationError as ValidationError,
)
from pydantic.v1 import create_model as create_model # type: ignore[no-redef]
from pydantic.v1.class_validators import Validator as Validator
from pydantic.v1.color import Color as Color # type: ignore[assignment]
from pydantic.v1.error_wrappers import ErrorWrapper as ErrorWrapper
from pydantic.v1.errors import MissingError
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 # type: ignore[assignment]
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 ( # type: ignore[assignment]
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 ( # type: ignore[assignment]
SecretBytes as SecretBytes,
)
from pydantic.v1.types import ( # type: ignore[assignment]
SecretStr as SecretStr,
)
from pydantic.v1.typing import evaluate_forwardref as evaluate_forwardref
from pydantic.v1.utils import lenient_issubclass as lenient_issubclass
GetJsonSchemaHandler = Any GetJsonSchemaHandler = Any
JsonSchemaValue = dict[str, Any] JsonSchemaValue = dict[str, Any]

6
fastapi/dependencies/utils.py

@ -18,7 +18,6 @@ from typing import (
import anyio import anyio
from fastapi import params from fastapi import params
from fastapi._compat import ( from fastapi._compat import (
PYDANTIC_V2,
ModelField, ModelField,
RequiredParam, RequiredParam,
Undefined, Undefined,
@ -445,9 +444,8 @@ def analyze_param(
f" together for {param_name!r}" f" together for {param_name!r}"
) )
field_info = value field_info = value
if PYDANTIC_V2: if isinstance(field_info, FieldInfo):
if isinstance(field_info, FieldInfo): field_info.annotation = type_annotation
field_info.annotation = type_annotation
# Get Depends from type annotation # Get Depends from type annotation
if depends is not None and depends.dependency is None: if depends is not None and depends.dependency is None:

17
fastapi/openapi/models.py

@ -3,7 +3,6 @@ from enum import Enum
from typing import Annotated, Any, Callable, Optional, Union from typing import Annotated, Any, Callable, Optional, Union
from fastapi._compat import ( from fastapi._compat import (
PYDANTIC_V2,
CoreSchema, CoreSchema,
GetJsonSchemaHandler, GetJsonSchemaHandler,
JsonSchemaValue, JsonSchemaValue,
@ -57,13 +56,7 @@ except ImportError: # pragma: no cover
class BaseModelWithConfig(BaseModel): class BaseModelWithConfig(BaseModel):
if PYDANTIC_V2: model_config = {"extra": "allow"}
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Contact(BaseModelWithConfig): class Contact(BaseModelWithConfig):
@ -226,13 +219,7 @@ class Example(TypedDict, total=False):
value: Optional[Any] value: Optional[Any]
externalValue: Optional[AnyUrl] externalValue: Optional[AnyUrl]
if PYDANTIC_V2: # type: ignore [misc] __pydantic_config__ = {"extra": "allow"}
__pydantic_config__ = {"extra": "allow"}
else:
class Config:
extra = "allow"
class ParameterInType(Enum): class ParameterInType(Enum):

71
fastapi/params.py

@ -9,7 +9,6 @@ from pydantic.fields import FieldInfo
from typing_extensions import Literal, deprecated from typing_extensions import Literal, deprecated
from ._compat import ( from ._compat import (
PYDANTIC_V2,
PYDANTIC_VERSION_MINOR_TUPLE, PYDANTIC_VERSION_MINOR_TUPLE,
Undefined, Undefined,
) )
@ -115,25 +114,23 @@ class Param(FieldInfo): # type: ignore[misc]
self.deprecated = deprecated self.deprecated = deprecated
else: else:
kwargs["deprecated"] = deprecated kwargs["deprecated"] = deprecated
if PYDANTIC_V2:
if serialization_alias in (_Unset, None) and isinstance(alias, str): if serialization_alias in (_Unset, None) and isinstance(alias, str):
serialization_alias = alias serialization_alias = alias
if validation_alias in (_Unset, None): if validation_alias in (_Unset, None):
validation_alias = alias validation_alias = alias
kwargs.update( kwargs.update(
{ {
"annotation": annotation, "annotation": annotation,
"alias_priority": alias_priority, "alias_priority": alias_priority,
"validation_alias": validation_alias, "validation_alias": validation_alias,
"serialization_alias": serialization_alias, "serialization_alias": serialization_alias,
"strict": strict, "strict": strict,
"json_schema_extra": current_json_schema_extra, "json_schema_extra": current_json_schema_extra,
} }
) )
kwargs["pattern"] = pattern or regex kwargs["pattern"] = pattern or regex
else:
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} use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset}
super().__init__(**use_kwargs) super().__init__(**use_kwargs)
@ -575,25 +572,21 @@ class Body(FieldInfo): # type: ignore[misc]
self.deprecated = deprecated self.deprecated = deprecated
else: else:
kwargs["deprecated"] = deprecated kwargs["deprecated"] = deprecated
if PYDANTIC_V2: if serialization_alias in (_Unset, None) and isinstance(alias, str):
if serialization_alias in (_Unset, None) and isinstance(alias, str): serialization_alias = alias
serialization_alias = alias if validation_alias in (_Unset, None):
if validation_alias in (_Unset, None): validation_alias = alias
validation_alias = alias kwargs.update(
kwargs.update( {
{ "annotation": annotation,
"annotation": annotation, "alias_priority": alias_priority,
"alias_priority": alias_priority, "validation_alias": validation_alias,
"validation_alias": validation_alias, "serialization_alias": serialization_alias,
"serialization_alias": serialization_alias, "strict": strict,
"strict": strict, "json_schema_extra": current_json_schema_extra,
"json_schema_extra": current_json_schema_extra, }
} )
) kwargs["pattern"] = pattern or regex
kwargs["pattern"] = pattern or regex
else:
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} use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset}

4
fastapi/routing.py

@ -30,6 +30,7 @@ from fastapi._compat import (
_model_dump, _model_dump,
_normalize_errors, _normalize_errors,
lenient_issubclass, lenient_issubclass,
may_v1,
) )
from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.datastructures import Default, DefaultPlaceholder
from fastapi.dependencies.models import Dependant from fastapi.dependencies.models import Dependant
@ -58,7 +59,6 @@ from fastapi.utils import (
get_value_or_default, get_value_or_default,
is_body_allowed_for_status_code, is_body_allowed_for_status_code,
) )
from pydantic import BaseModel
from starlette import routing from starlette import routing
from starlette._exception_handler import wrap_app_handling_exceptions from starlette._exception_handler import wrap_app_handling_exceptions
from starlette._utils import is_async_callable from starlette._utils import is_async_callable
@ -153,7 +153,7 @@ def _prepare_response_content(
exclude_defaults: bool = False, exclude_defaults: bool = False,
exclude_none: bool = False, exclude_none: bool = False,
) -> Any: ) -> Any:
if isinstance(res, BaseModel): if isinstance(res, may_v1.BaseModel):
read_with_orm_mode = getattr(_get_model_config(res), "read_with_orm_mode", None) read_with_orm_mode = getattr(_get_model_config(res), "read_with_orm_mode", None)
if read_with_orm_mode: if read_with_orm_mode:
# Let from_orm extract the data from this model instead of converting # Let from_orm extract the data from this model instead of converting

14
fastapi/utils.py

@ -13,7 +13,6 @@ from weakref import WeakKeyDictionary
import fastapi import fastapi
from fastapi._compat import ( from fastapi._compat import (
PYDANTIC_V2,
BaseConfig, BaseConfig,
ModelField, ModelField,
PydanticSchemaGenerationError, PydanticSchemaGenerationError,
@ -29,6 +28,8 @@ from pydantic import BaseModel
from pydantic.fields import FieldInfo from pydantic.fields import FieldInfo
from typing_extensions import Literal from typing_extensions import Literal
from ._compat import v2
if TYPE_CHECKING: # pragma: nocover if TYPE_CHECKING: # pragma: nocover
from .routing import APIRoute from .routing import APIRoute
@ -110,9 +111,7 @@ def create_model_field(
raise fastapi.exceptions.FastAPIError( raise fastapi.exceptions.FastAPIError(
_invalid_args_message.format(type_=type_) _invalid_args_message.format(type_=type_)
) from None ) from None
elif PYDANTIC_V2: else:
from ._compat import v2
field_info = field_info or FieldInfo( field_info = field_info or FieldInfo(
annotation=type_, default=default, alias=alias annotation=type_, default=default, alias=alias
) )
@ -140,11 +139,8 @@ def create_cloned_field(
*, *,
cloned_types: Optional[MutableMapping[type[BaseModel], type[BaseModel]]] = None, cloned_types: Optional[MutableMapping[type[BaseModel], type[BaseModel]]] = None,
) -> ModelField: ) -> ModelField:
if PYDANTIC_V2: if isinstance(field, v2.ModelField):
from ._compat import v2 return field
if isinstance(field, v2.ModelField):
return field
from fastapi._compat import v1 from fastapi._compat import v1

8
tests/test_additional_properties_bool.py

@ -2,18 +2,12 @@ from typing import Union
from dirty_equals import IsDict from dirty_equals import IsDict
from fastapi import FastAPI from fastapi import FastAPI
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
class FooBaseModel(BaseModel): class FooBaseModel(BaseModel):
if PYDANTIC_V2: model_config = ConfigDict(extra="forbid")
model_config = ConfigDict(extra="forbid")
else:
class Config:
extra = "forbid"
class Foo(FooBaseModel): class Foo(FooBaseModel):

6
tests/test_ambiguous_params.py

@ -4,7 +4,6 @@ import pytest
from fastapi import Depends, FastAPI, Path from fastapi import Depends, FastAPI, Path
from fastapi.param_functions import Query from fastapi.param_functions import Query
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import PYDANTIC_V2
app = FastAPI() app = FastAPI()
@ -71,6 +70,5 @@ def test_multiple_annotations():
response = client.get("/multi-query", params={"foo": "123"}) response = client.get("/multi-query", params={"foo": "123"})
assert response.status_code == 422 assert response.status_code == 422
if PYDANTIC_V2: response = client.get("/multi-query", params={"foo": "1"})
response = client.get("/multi-query", params={"foo": "1"}) assert response.status_code == 422
assert response.status_code == 422

30
tests/test_custom_schema_fields.py

@ -1,12 +1,8 @@
from typing import Annotated, Optional from typing import Annotated, Optional
from fastapi import FastAPI from fastapi import FastAPI
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel from pydantic import BaseModel, WithJsonSchema
if PYDANTIC_V2:
from pydantic import WithJsonSchema
app = FastAPI() app = FastAPI()
@ -14,23 +10,15 @@ app = FastAPI()
class Item(BaseModel): class Item(BaseModel):
name: str name: str
if PYDANTIC_V2: description: Annotated[
description: Annotated[ Optional[str], WithJsonSchema({"type": ["string", "null"]})
Optional[str], WithJsonSchema({"type": ["string", "null"]}) ] = None
] = None
model_config = { model_config = {
"json_schema_extra": { "json_schema_extra": {
"x-something-internal": {"level": 4}, "x-something-internal": {"level": 4},
}
} }
else: }
description: Optional[str] = None # type: ignore[no-redef]
class Config:
schema_extra = {
"x-something-internal": {"level": 4},
}
@app.get("/foo", response_model=Item) @app.get("/foo", response_model=Item)
@ -55,7 +43,7 @@ item_schema = {
}, },
"description": { "description": {
"title": "Description", "title": "Description",
"type": ["string", "null"] if PYDANTIC_V2 else "string", "type": ["string", "null"],
}, },
}, },
} }

8
tests/test_forms_single_model.py

@ -2,7 +2,6 @@ from typing import Annotated, Optional
from dirty_equals import IsDict from dirty_equals import IsDict
from fastapi import FastAPI, Form from fastapi import FastAPI, Form
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@ -20,12 +19,7 @@ class FormModel(BaseModel):
class FormModelExtraAllow(BaseModel): class FormModelExtraAllow(BaseModel):
param: str param: str
if PYDANTIC_V2: model_config = {"extra": "allow"}
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
@app.post("/form/") @app.post("/form/")

33
tests/test_jsonable_encoder.py

@ -8,7 +8,7 @@ from pathlib import PurePath, PurePosixPath, PureWindowsPath
from typing import Optional from typing import Optional
import pytest import pytest
from fastapi._compat import PYDANTIC_V2, Undefined from fastapi._compat import Undefined
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel, Field, ValidationError from pydantic import BaseModel, Field, ValidationError
@ -59,12 +59,7 @@ class RoleEnum(Enum):
class ModelWithConfig(BaseModel): class ModelWithConfig(BaseModel):
role: Optional[RoleEnum] = None role: Optional[RoleEnum] = None
if PYDANTIC_V2: model_config = {"use_enum_values": True}
model_config = {"use_enum_values": True}
else:
class Config:
use_enum_values = True
class ModelWithAlias(BaseModel): class ModelWithAlias(BaseModel):
@ -154,6 +149,7 @@ def test_encode_custom_json_encoders_model_pydanticv2():
@needs_pydanticv1 @needs_pydanticv1
def test_encode_custom_json_encoders_model_pydanticv1(): def test_encode_custom_json_encoders_model_pydanticv1():
from pydantic import v1 from pydantic import v1
class ModelWithCustomEncoder(v1.BaseModel): class ModelWithCustomEncoder(v1.BaseModel):
dt_field: datetime dt_field: datetime
@ -210,6 +206,7 @@ def test_encode_model_with_default():
@needs_pydanticv1 @needs_pydanticv1
def test_custom_encoders(): def test_custom_encoders():
from pydantic import v1 from pydantic import v1
class safe_datetime(datetime): class safe_datetime(datetime):
pass pass
@ -246,12 +243,7 @@ def test_encode_model_with_pure_path():
class ModelWithPath(BaseModel): class ModelWithPath(BaseModel):
path: PurePath path: PurePath
if PYDANTIC_V2: model_config = {"arbitrary_types_allowed": True}
model_config = {"arbitrary_types_allowed": True}
else:
class Config:
arbitrary_types_allowed = True
test_path = PurePath("/foo", "bar") test_path = PurePath("/foo", "bar")
obj = ModelWithPath(path=test_path) obj = ModelWithPath(path=test_path)
@ -262,12 +254,7 @@ def test_encode_model_with_pure_posix_path():
class ModelWithPath(BaseModel): class ModelWithPath(BaseModel):
path: PurePosixPath path: PurePosixPath
if PYDANTIC_V2: model_config = {"arbitrary_types_allowed": True}
model_config = {"arbitrary_types_allowed": True}
else:
class Config:
arbitrary_types_allowed = True
obj = ModelWithPath(path=PurePosixPath("/foo", "bar")) obj = ModelWithPath(path=PurePosixPath("/foo", "bar"))
assert jsonable_encoder(obj) == {"path": "/foo/bar"} assert jsonable_encoder(obj) == {"path": "/foo/bar"}
@ -277,12 +264,7 @@ def test_encode_model_with_pure_windows_path():
class ModelWithPath(BaseModel): class ModelWithPath(BaseModel):
path: PureWindowsPath path: PureWindowsPath
if PYDANTIC_V2: model_config = {"arbitrary_types_allowed": True}
model_config = {"arbitrary_types_allowed": True}
else:
class Config:
arbitrary_types_allowed = True
obj = ModelWithPath(path=PureWindowsPath("/foo", "bar")) obj = ModelWithPath(path=PureWindowsPath("/foo", "bar"))
assert jsonable_encoder(obj) == {"path": "\\foo\\bar"} assert jsonable_encoder(obj) == {"path": "\\foo\\bar"}
@ -291,6 +273,7 @@ def test_encode_model_with_pure_windows_path():
@needs_pydanticv1 @needs_pydanticv1
def test_encode_root(): def test_encode_root():
from pydantic import v1 from pydantic import v1
class ModelWithRoot(v1.BaseModel): class ModelWithRoot(v1.BaseModel):
__root__: str __root__: str

37
tests/test_openapi_separate_input_output_schemas.py

@ -3,37 +3,32 @@ from typing import Optional
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from inline_snapshot import snapshot from inline_snapshot import snapshot
from pydantic import BaseModel from pydantic import BaseModel, computed_field
from .utils import PYDANTIC_V2, needs_pydanticv2 from .utils import needs_pydanticv2
class SubItem(BaseModel): class SubItem(BaseModel):
subname: str subname: str
sub_description: Optional[str] = None sub_description: Optional[str] = None
tags: list[str] = [] tags: list[str] = []
if PYDANTIC_V2: model_config = {"json_schema_serialization_defaults_required": True}
model_config = {"json_schema_serialization_defaults_required": True}
class Item(BaseModel): class Item(BaseModel):
name: str name: str
description: Optional[str] = None description: Optional[str] = None
sub: Optional[SubItem] = None sub: Optional[SubItem] = None
if PYDANTIC_V2: model_config = {"json_schema_serialization_defaults_required": True}
model_config = {"json_schema_serialization_defaults_required": True}
if PYDANTIC_V2: class WithComputedField(BaseModel):
from pydantic import computed_field name: str
class WithComputedField(BaseModel):
name: str
@computed_field @computed_field
@property @property
def computed_field(self) -> str: def computed_field(self) -> str:
return f"computed {self.name}" return f"computed {self.name}"
def get_app_client(separate_input_output_schemas: bool = True) -> TestClient: def get_app_client(separate_input_output_schemas: bool = True) -> TestClient:
@ -58,13 +53,11 @@ def get_app_client(separate_input_output_schemas: bool = True) -> TestClient:
Item(name="Plumbus"), Item(name="Plumbus"),
] ]
if PYDANTIC_V2: @app.post("/with-computed-field/")
def create_with_computed_field(
@app.post("/with-computed-field/") with_computed_field: WithComputedField,
def create_with_computed_field( ) -> WithComputedField:
with_computed_field: WithComputedField, return with_computed_field
) -> WithComputedField:
return with_computed_field
client = TestClient(app) client = TestClient(app)
return client return client

8
tests/test_query_cookie_header_model_extra_params.py

@ -1,5 +1,4 @@
from fastapi import Cookie, FastAPI, Header, Query from fastapi import Cookie, FastAPI, Header, Query
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel from pydantic import BaseModel
@ -9,12 +8,7 @@ app = FastAPI()
class Model(BaseModel): class Model(BaseModel):
param: str param: str
if PYDANTIC_V2: model_config = {"extra": "allow"}
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
@app.get("/query") @app.get("/query")

10
tests/test_request_param_model_by_alias.py

@ -1,6 +1,5 @@
from dirty_equals import IsPartialDict from dirty_equals import IsPartialDict
from fastapi import Cookie, FastAPI, Header, Query from fastapi import Cookie, FastAPI, Header, Query
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@ -53,8 +52,7 @@ def test_query_model_with_alias_by_name():
response = client.get("/query", params={"param": "value"}) response = client.get("/query", params={"param": "value"})
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
details = response.json() details = response.json()
if PYDANTIC_V2: assert details["detail"][0]["input"] == {"param": "value"}
assert details["detail"][0]["input"] == {"param": "value"}
def test_header_model_with_alias_by_name(): def test_header_model_with_alias_by_name():
@ -62,8 +60,7 @@ def test_header_model_with_alias_by_name():
response = client.get("/header", headers={"param": "value"}) response = client.get("/header", headers={"param": "value"})
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
details = response.json() details = response.json()
if PYDANTIC_V2: assert details["detail"][0]["input"] == IsPartialDict({"param": "value"})
assert details["detail"][0]["input"] == IsPartialDict({"param": "value"})
def test_cookie_model_with_alias_by_name(): def test_cookie_model_with_alias_by_name():
@ -72,5 +69,4 @@ def test_cookie_model_with_alias_by_name():
response = client.get("/cookie") response = client.get("/cookie")
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
details = response.json() details = response.json()
if PYDANTIC_V2: assert details["detail"][0]["input"] == {"param": "value"}
assert details["detail"][0]["input"] == {"param": "value"}

27
tests/test_response_by_alias.py

@ -1,5 +1,4 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
@ -13,24 +12,14 @@ class Model(BaseModel):
class ModelNoAlias(BaseModel): class ModelNoAlias(BaseModel):
name: str name: str
if PYDANTIC_V2: model_config = ConfigDict(
model_config = ConfigDict( json_schema_extra={
json_schema_extra={ "description": (
"description": ( "response_model_by_alias=False is basically a quick hack, to support "
"response_model_by_alias=False is basically a quick hack, to support " "proper OpenAPI use another model with the correct field names"
"proper OpenAPI use another model with the correct field names" )
) }
} )
)
else:
class Config:
schema_extra = {
"description": (
"response_model_by_alias=False is basically a quick hack, to support "
"proper OpenAPI use another model with the correct field names"
)
}
@app.get("/dict", response_model=Model, response_model_by_alias=False) @app.get("/dict", response_model=Model, response_model_by_alias=False)

12
tests/test_schema_extra_examples.py

@ -3,7 +3,6 @@ from typing import Union
import pytest import pytest
from dirty_equals import IsDict from dirty_equals import IsDict
from fastapi import Body, Cookie, FastAPI, Header, Path, Query from fastapi import Body, Cookie, FastAPI, Header, Path, Query
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
@ -14,14 +13,9 @@ def create_app():
class Item(BaseModel): class Item(BaseModel):
data: str data: str
if PYDANTIC_V2: model_config = ConfigDict(
model_config = ConfigDict( json_schema_extra={"example": {"data": "Data in schema_extra"}}
json_schema_extra={"example": {"data": "Data in schema_extra"}} )
)
else:
class Config:
schema_extra = {"example": {"data": "Data in schema_extra"}}
@app.post("/schema_extra/") @app.post("/schema_extra/")
def schema_extra(item: Item): def schema_extra(item: Item):

6
tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py

@ -2,7 +2,6 @@ import sys
from typing import Any from typing import Any
import pytest import pytest
from fastapi._compat import PYDANTIC_V2
from tests.utils import skip_module_if_py_gte_314 from tests.utils import skip_module_if_py_gte_314
@ -10,13 +9,8 @@ if sys.version_info >= (3, 14):
skip_module_if_py_gte_314() skip_module_if_py_gte_314()
if not PYDANTIC_V2:
pytest.skip("This test is only for Pydantic v2", allow_module_level=True)
import importlib import importlib
import pytest
from ...utils import needs_py310 from ...utils import needs_py310

5
tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py

@ -1,7 +1,6 @@
import sys import sys
import pytest import pytest
from fastapi._compat import PYDANTIC_V2
from inline_snapshot import snapshot from inline_snapshot import snapshot
from tests.utils import skip_module_if_py_gte_314 from tests.utils import skip_module_if_py_gte_314
@ -10,12 +9,8 @@ if sys.version_info >= (3, 14):
skip_module_if_py_gte_314() skip_module_if_py_gte_314()
if not PYDANTIC_V2:
pytest.skip("This test is only for Pydantic v2", allow_module_level=True)
import importlib import importlib
import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from ...utils import needs_py310 from ...utils import needs_py310

4
tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py

@ -1,7 +1,6 @@
import sys import sys
import pytest import pytest
from fastapi._compat import PYDANTIC_V2
from inline_snapshot import snapshot from inline_snapshot import snapshot
from tests.utils import skip_module_if_py_gte_314 from tests.utils import skip_module_if_py_gte_314
@ -9,9 +8,6 @@ from tests.utils import skip_module_if_py_gte_314
if sys.version_info >= (3, 14): if sys.version_info >= (3, 14):
skip_module_if_py_gte_314() skip_module_if_py_gte_314()
if not PYDANTIC_V2:
pytest.skip("This test is only for Pydantic v2", allow_module_level=True)
import importlib import importlib

4
tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py

@ -1,7 +1,6 @@
import sys import sys
import pytest import pytest
from fastapi._compat import PYDANTIC_V2
from inline_snapshot import snapshot from inline_snapshot import snapshot
from tests.utils import skip_module_if_py_gte_314 from tests.utils import skip_module_if_py_gte_314
@ -9,9 +8,6 @@ from tests.utils import skip_module_if_py_gte_314
if sys.version_info >= (3, 14): if sys.version_info >= (3, 14):
skip_module_if_py_gte_314() skip_module_if_py_gte_314()
if not PYDANTIC_V2:
pytest.skip("This test is only for Pydantic v2", allow_module_level=True)
import importlib import importlib

11
tests/test_validate_response_recursive/app.py

@ -1,5 +1,4 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi._compat import PYDANTIC_V2
from pydantic import BaseModel from pydantic import BaseModel
app = FastAPI() app = FastAPI()
@ -20,13 +19,9 @@ class RecursiveItemViaSubmodel(BaseModel):
name: str name: str
if PYDANTIC_V2: RecursiveItem.model_rebuild()
RecursiveItem.model_rebuild() RecursiveSubitemInSubmodel.model_rebuild()
RecursiveSubitemInSubmodel.model_rebuild() RecursiveItemViaSubmodel.model_rebuild()
RecursiveItemViaSubmodel.model_rebuild()
else:
RecursiveItem.update_forward_refs()
RecursiveSubitemInSubmodel.update_forward_refs()
@app.get("/items/recursive", response_model=RecursiveItem) @app.get("/items/recursive", response_model=RecursiveItem)

Loading…
Cancel
Save