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

147
fastapi/_compat/v1.py

@ -11,6 +11,56 @@ from typing import (
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 # 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 typing_extensions import Literal
@ -20,103 +70,6 @@ PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2
# shadowing typing.Required.
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
JsonSchemaValue = dict[str, Any]

6
fastapi/dependencies/utils.py

@ -18,7 +18,6 @@ from typing import (
import anyio
from fastapi import params
from fastapi._compat import (
PYDANTIC_V2,
ModelField,
RequiredParam,
Undefined,
@ -445,9 +444,8 @@ def analyze_param(
f" together for {param_name!r}"
)
field_info = value
if PYDANTIC_V2:
if isinstance(field_info, FieldInfo):
field_info.annotation = type_annotation
if isinstance(field_info, FieldInfo):
field_info.annotation = type_annotation
# Get Depends from type annotation
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 fastapi._compat import (
PYDANTIC_V2,
CoreSchema,
GetJsonSchemaHandler,
JsonSchemaValue,
@ -57,13 +56,7 @@ except ImportError: # pragma: no cover
class BaseModelWithConfig(BaseModel):
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
model_config = {"extra": "allow"}
class Contact(BaseModelWithConfig):
@ -226,13 +219,7 @@ class Example(TypedDict, total=False):
value: Optional[Any]
externalValue: Optional[AnyUrl]
if PYDANTIC_V2: # type: ignore [misc]
__pydantic_config__ = {"extra": "allow"}
else:
class Config:
extra = "allow"
__pydantic_config__ = {"extra": "allow"}
class ParameterInType(Enum):

71
fastapi/params.py

@ -9,7 +9,6 @@ from pydantic.fields import FieldInfo
from typing_extensions import Literal, deprecated
from ._compat import (
PYDANTIC_V2,
PYDANTIC_VERSION_MINOR_TUPLE,
Undefined,
)
@ -115,25 +114,23 @@ class Param(FieldInfo): # type: ignore[misc]
self.deprecated = deprecated
else:
kwargs["deprecated"] = deprecated
if PYDANTIC_V2:
if serialization_alias in (_Unset, None) and isinstance(alias, str):
serialization_alias = alias
if validation_alias in (_Unset, None):
validation_alias = alias
kwargs.update(
{
"annotation": annotation,
"alias_priority": alias_priority,
"validation_alias": validation_alias,
"serialization_alias": serialization_alias,
"strict": strict,
"json_schema_extra": current_json_schema_extra,
}
)
kwargs["pattern"] = pattern or regex
else:
kwargs["regex"] = pattern or regex
kwargs.update(**current_json_schema_extra)
if serialization_alias in (_Unset, None) and isinstance(alias, str):
serialization_alias = alias
if validation_alias in (_Unset, None):
validation_alias = alias
kwargs.update(
{
"annotation": annotation,
"alias_priority": alias_priority,
"validation_alias": validation_alias,
"serialization_alias": serialization_alias,
"strict": strict,
"json_schema_extra": current_json_schema_extra,
}
)
kwargs["pattern"] = pattern or regex
use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset}
super().__init__(**use_kwargs)
@ -575,25 +572,21 @@ class Body(FieldInfo): # type: ignore[misc]
self.deprecated = deprecated
else:
kwargs["deprecated"] = deprecated
if PYDANTIC_V2:
if serialization_alias in (_Unset, None) and isinstance(alias, str):
serialization_alias = alias
if validation_alias in (_Unset, None):
validation_alias = alias
kwargs.update(
{
"annotation": annotation,
"alias_priority": alias_priority,
"validation_alias": validation_alias,
"serialization_alias": serialization_alias,
"strict": strict,
"json_schema_extra": current_json_schema_extra,
}
)
kwargs["pattern"] = pattern or regex
else:
kwargs["regex"] = pattern or regex
kwargs.update(**current_json_schema_extra)
if serialization_alias in (_Unset, None) and isinstance(alias, str):
serialization_alias = alias
if validation_alias in (_Unset, None):
validation_alias = alias
kwargs.update(
{
"annotation": annotation,
"alias_priority": alias_priority,
"validation_alias": validation_alias,
"serialization_alias": serialization_alias,
"strict": strict,
"json_schema_extra": current_json_schema_extra,
}
)
kwargs["pattern"] = pattern or regex
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,
_normalize_errors,
lenient_issubclass,
may_v1,
)
from fastapi.datastructures import Default, DefaultPlaceholder
from fastapi.dependencies.models import Dependant
@ -58,7 +59,6 @@ from fastapi.utils import (
get_value_or_default,
is_body_allowed_for_status_code,
)
from pydantic import BaseModel
from starlette import routing
from starlette._exception_handler import wrap_app_handling_exceptions
from starlette._utils import is_async_callable
@ -153,7 +153,7 @@ def _prepare_response_content(
exclude_defaults: bool = False,
exclude_none: bool = False,
) -> 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)
if read_with_orm_mode:
# 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
from fastapi._compat import (
PYDANTIC_V2,
BaseConfig,
ModelField,
PydanticSchemaGenerationError,
@ -29,6 +28,8 @@ from pydantic import BaseModel
from pydantic.fields import FieldInfo
from typing_extensions import Literal
from ._compat import v2
if TYPE_CHECKING: # pragma: nocover
from .routing import APIRoute
@ -110,9 +111,7 @@ def create_model_field(
raise fastapi.exceptions.FastAPIError(
_invalid_args_message.format(type_=type_)
) from None
elif PYDANTIC_V2:
from ._compat import v2
else:
field_info = field_info or FieldInfo(
annotation=type_, default=default, alias=alias
)
@ -140,11 +139,8 @@ def create_cloned_field(
*,
cloned_types: Optional[MutableMapping[type[BaseModel], type[BaseModel]]] = None,
) -> ModelField:
if PYDANTIC_V2:
from ._compat import v2
if isinstance(field, v2.ModelField):
return field
if isinstance(field, v2.ModelField):
return field
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 fastapi import FastAPI
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict
class FooBaseModel(BaseModel):
if PYDANTIC_V2:
model_config = ConfigDict(extra="forbid")
else:
class Config:
extra = "forbid"
model_config = ConfigDict(extra="forbid")
class Foo(FooBaseModel):

6
tests/test_ambiguous_params.py

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

30
tests/test_custom_schema_fields.py

@ -1,12 +1,8 @@
from typing import Annotated, Optional
from fastapi import FastAPI
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel
if PYDANTIC_V2:
from pydantic import WithJsonSchema
from pydantic import BaseModel, WithJsonSchema
app = FastAPI()
@ -14,23 +10,15 @@ app = FastAPI()
class Item(BaseModel):
name: str
if PYDANTIC_V2:
description: Annotated[
Optional[str], WithJsonSchema({"type": ["string", "null"]})
] = None
description: Annotated[
Optional[str], WithJsonSchema({"type": ["string", "null"]})
] = None
model_config = {
"json_schema_extra": {
"x-something-internal": {"level": 4},
}
model_config = {
"json_schema_extra": {
"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)
@ -55,7 +43,7 @@ item_schema = {
},
"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 fastapi import FastAPI, Form
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
@ -20,12 +19,7 @@ class FormModel(BaseModel):
class FormModelExtraAllow(BaseModel):
param: str
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
model_config = {"extra": "allow"}
@app.post("/form/")

33
tests/test_jsonable_encoder.py

@ -8,7 +8,7 @@ from pathlib import PurePath, PurePosixPath, PureWindowsPath
from typing import Optional
import pytest
from fastapi._compat import PYDANTIC_V2, Undefined
from fastapi._compat import Undefined
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel, Field, ValidationError
@ -59,12 +59,7 @@ class RoleEnum(Enum):
class ModelWithConfig(BaseModel):
role: Optional[RoleEnum] = None
if PYDANTIC_V2:
model_config = {"use_enum_values": True}
else:
class Config:
use_enum_values = True
model_config = {"use_enum_values": True}
class ModelWithAlias(BaseModel):
@ -154,6 +149,7 @@ def test_encode_custom_json_encoders_model_pydanticv2():
@needs_pydanticv1
def test_encode_custom_json_encoders_model_pydanticv1():
from pydantic import v1
class ModelWithCustomEncoder(v1.BaseModel):
dt_field: datetime
@ -210,6 +206,7 @@ def test_encode_model_with_default():
@needs_pydanticv1
def test_custom_encoders():
from pydantic import v1
class safe_datetime(datetime):
pass
@ -246,12 +243,7 @@ def test_encode_model_with_pure_path():
class ModelWithPath(BaseModel):
path: PurePath
if PYDANTIC_V2:
model_config = {"arbitrary_types_allowed": True}
else:
class Config:
arbitrary_types_allowed = True
model_config = {"arbitrary_types_allowed": True}
test_path = PurePath("/foo", "bar")
obj = ModelWithPath(path=test_path)
@ -262,12 +254,7 @@ def test_encode_model_with_pure_posix_path():
class ModelWithPath(BaseModel):
path: PurePosixPath
if PYDANTIC_V2:
model_config = {"arbitrary_types_allowed": True}
else:
class Config:
arbitrary_types_allowed = True
model_config = {"arbitrary_types_allowed": True}
obj = ModelWithPath(path=PurePosixPath("/foo", "bar"))
assert jsonable_encoder(obj) == {"path": "/foo/bar"}
@ -277,12 +264,7 @@ def test_encode_model_with_pure_windows_path():
class ModelWithPath(BaseModel):
path: PureWindowsPath
if PYDANTIC_V2:
model_config = {"arbitrary_types_allowed": True}
else:
class Config:
arbitrary_types_allowed = True
model_config = {"arbitrary_types_allowed": True}
obj = ModelWithPath(path=PureWindowsPath("/foo", "bar"))
assert jsonable_encoder(obj) == {"path": "\\foo\\bar"}
@ -291,6 +273,7 @@ def test_encode_model_with_pure_windows_path():
@needs_pydanticv1
def test_encode_root():
from pydantic import v1
class ModelWithRoot(v1.BaseModel):
__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.testclient import TestClient
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):
subname: str
sub_description: Optional[str] = None
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):
name: str
description: Optional[str] = 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:
from pydantic import computed_field
class WithComputedField(BaseModel):
name: str
class WithComputedField(BaseModel):
name: str
@computed_field
@property
def computed_field(self) -> str:
return f"computed {self.name}"
@computed_field
@property
def computed_field(self) -> str:
return f"computed {self.name}"
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"),
]
if PYDANTIC_V2:
@app.post("/with-computed-field/")
def create_with_computed_field(
with_computed_field: WithComputedField,
) -> WithComputedField:
return with_computed_field
@app.post("/with-computed-field/")
def create_with_computed_field(
with_computed_field: WithComputedField,
) -> WithComputedField:
return with_computed_field
client = TestClient(app)
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._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel
@ -9,12 +8,7 @@ app = FastAPI()
class Model(BaseModel):
param: str
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
model_config = {"extra": "allow"}
@app.get("/query")

10
tests/test_request_param_model_by_alias.py

@ -1,6 +1,5 @@
from dirty_equals import IsPartialDict
from fastapi import Cookie, FastAPI, Header, Query
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
@ -53,8 +52,7 @@ def test_query_model_with_alias_by_name():
response = client.get("/query", params={"param": "value"})
assert response.status_code == 422, response.text
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():
@ -62,8 +60,7 @@ def test_header_model_with_alias_by_name():
response = client.get("/header", headers={"param": "value"})
assert response.status_code == 422, response.text
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():
@ -72,5 +69,4 @@ def test_cookie_model_with_alias_by_name():
response = client.get("/cookie")
assert response.status_code == 422, response.text
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._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict, Field
@ -13,24 +12,14 @@ class Model(BaseModel):
class ModelNoAlias(BaseModel):
name: str
if PYDANTIC_V2:
model_config = ConfigDict(
json_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"
)
}
)
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"
)
}
model_config = ConfigDict(
json_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)

12
tests/test_schema_extra_examples.py

@ -3,7 +3,6 @@ from typing import Union
import pytest
from dirty_equals import IsDict
from fastapi import Body, Cookie, FastAPI, Header, Path, Query
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict
@ -14,14 +13,9 @@ def create_app():
class Item(BaseModel):
data: str
if PYDANTIC_V2:
model_config = ConfigDict(
json_schema_extra={"example": {"data": "Data in schema_extra"}}
)
else:
class Config:
schema_extra = {"example": {"data": "Data in schema_extra"}}
model_config = ConfigDict(
json_schema_extra={"example": {"data": "Data in schema_extra"}}
)
@app.post("/schema_extra/")
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
import pytest
from fastapi._compat import PYDANTIC_V2
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()
if not PYDANTIC_V2:
pytest.skip("This test is only for Pydantic v2", allow_module_level=True)
import importlib
import pytest
from ...utils import needs_py310

5
tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py

@ -1,7 +1,6 @@
import sys
import pytest
from fastapi._compat import PYDANTIC_V2
from inline_snapshot import snapshot
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()
if not PYDANTIC_V2:
pytest.skip("This test is only for Pydantic v2", allow_module_level=True)
import importlib
import pytest
from fastapi.testclient import TestClient
from ...utils import needs_py310

4
tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py

@ -1,7 +1,6 @@
import sys
import pytest
from fastapi._compat import PYDANTIC_V2
from inline_snapshot import snapshot
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):
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

4
tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py

@ -1,7 +1,6 @@
import sys
import pytest
from fastapi._compat import PYDANTIC_V2
from inline_snapshot import snapshot
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):
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

11
tests/test_validate_response_recursive/app.py

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

Loading…
Cancel
Save