From 00c8b734938cf70c6b6730187b5ad394d87bc13d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 20 Dec 2025 13:15:40 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A5=20Remove=20conditionals=20checking?= =?UTF-8?q?=20if=20Pydantic=20v1=20was=20installed,=20first=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/_compat/main.py | 187 +++++------------- fastapi/_compat/v1.py | 147 +++++--------- fastapi/dependencies/utils.py | 6 +- fastapi/openapi/models.py | 17 +- fastapi/params.py | 71 +++---- fastapi/routing.py | 4 +- fastapi/utils.py | 14 +- tests/test_additional_properties_bool.py | 8 +- tests/test_ambiguous_params.py | 6 +- tests/test_custom_schema_fields.py | 30 +-- tests/test_forms_single_model.py | 8 +- tests/test_jsonable_encoder.py | 33 +--- ...t_openapi_separate_input_output_schemas.py | 37 ++-- ..._query_cookie_header_model_extra_params.py | 8 +- tests/test_request_param_model_by_alias.py | 10 +- tests/test_response_by_alias.py | 27 +-- tests/test_schema_extra_examples.py | 12 +- .../test_tutorial001.py | 6 - .../test_tutorial002.py | 5 - .../test_tutorial003.py | 4 - .../test_tutorial004.py | 4 - tests/test_validate_response_recursive/app.py | 11 +- 22 files changed, 200 insertions(+), 455 deletions(-) diff --git a/fastapi/_compat/main.py b/fastapi/_compat/main.py index 2043a6678..bb4d7e716 100644 --- a/fastapi/_compat/main.py +++ b/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] diff --git a/fastapi/_compat/v1.py b/fastapi/_compat/v1.py index b29a61f73..e5659c94d 100644 --- a/fastapi/_compat/v1.py +++ b/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] diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 3db006a91..8f34acae0 100644 --- a/fastapi/dependencies/utils.py +++ b/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: diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index 7aa80f5cb..e60f48b4c 100644 --- a/fastapi/openapi/models.py +++ b/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): diff --git a/fastapi/params.py b/fastapi/params.py index 4990d0e70..97d57a2a5 100644 --- a/fastapi/params.py +++ b/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} diff --git a/fastapi/routing.py b/fastapi/routing.py index fa6904a6b..142286d55 100644 --- a/fastapi/routing.py +++ b/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 diff --git a/fastapi/utils.py b/fastapi/utils.py index 5b93b9af8..425ac1fbd 100644 --- a/fastapi/utils.py +++ b/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 diff --git a/tests/test_additional_properties_bool.py b/tests/test_additional_properties_bool.py index de59e48ce..3756b7d7c 100644 --- a/tests/test_additional_properties_bool.py +++ b/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): diff --git a/tests/test_ambiguous_params.py b/tests/test_ambiguous_params.py index 5e48782f8..44d49b781 100644 --- a/tests/test_ambiguous_params.py +++ b/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 diff --git a/tests/test_custom_schema_fields.py b/tests/test_custom_schema_fields.py index bc3e1c5ec..60b795e9b 100644 --- a/tests/test_custom_schema_fields.py +++ b/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"], }, }, } diff --git a/tests/test_forms_single_model.py b/tests/test_forms_single_model.py index b149b7653..c401cc937 100644 --- a/tests/test_forms_single_model.py +++ b/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/") diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index 45f756467..e78c1788e 100644 --- a/tests/test_jsonable_encoder.py +++ b/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 diff --git a/tests/test_openapi_separate_input_output_schemas.py b/tests/test_openapi_separate_input_output_schemas.py index bfe5e9a71..ab2a65d29 100644 --- a/tests/test_openapi_separate_input_output_schemas.py +++ b/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 diff --git a/tests/test_query_cookie_header_model_extra_params.py b/tests/test_query_cookie_header_model_extra_params.py index f4ebefb3f..d361e1e53 100644 --- a/tests/test_query_cookie_header_model_extra_params.py +++ b/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") diff --git a/tests/test_request_param_model_by_alias.py b/tests/test_request_param_model_by_alias.py index a6f759f23..c29130d7a 100644 --- a/tests/test_request_param_model_by_alias.py +++ b/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"} diff --git a/tests/test_response_by_alias.py b/tests/test_response_by_alias.py index 5b241c76b..807d2600b 100644 --- a/tests/test_response_by_alias.py +++ b/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) diff --git a/tests/test_schema_extra_examples.py b/tests/test_schema_extra_examples.py index b313f47e9..176b5588d 100644 --- a/tests/test_schema_extra_examples.py +++ b/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): 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 index ce53a36eb..4090eba01 100644 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py +++ b/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 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 index 57720bf48..266d25944 100644 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py +++ b/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 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 index f020d4a97..693c3ba29 100644 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py +++ b/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 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 index daa6f0b05..0fd084c84 100644 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py +++ b/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 diff --git a/tests/test_validate_response_recursive/app.py b/tests/test_validate_response_recursive/app.py index 8f76572ba..d51aa848d 100644 --- a/tests/test_validate_response_recursive/app.py +++ b/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)