Browse Source

♻️ Refactor internals, simplify Pydantic v2/v1 utils, `create_model_field`, better types for `lenient_issubclass` (#14860)

pull/14862/head
Sebastián Ramírez 5 months ago
committed by GitHub
parent
commit
d59fbc3494
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 7
      fastapi/_compat/__init__.py
  2. 37
      fastapi/_compat/shared.py
  3. 40
      fastapi/_compat/v2.py
  4. 8
      fastapi/dependencies/utils.py
  5. 5
      fastapi/openapi/models.py
  6. 6
      fastapi/openapi/utils.py
  7. 12
      fastapi/routing.py
  8. 12
      fastapi/utils.py

7
fastapi/_compat/__init__.py

@ -1,7 +1,6 @@
from .shared import PYDANTIC_VERSION_MINOR_TUPLE as PYDANTIC_VERSION_MINOR_TUPLE from .shared import PYDANTIC_VERSION_MINOR_TUPLE as PYDANTIC_VERSION_MINOR_TUPLE
from .shared import annotation_is_pydantic_v1 as annotation_is_pydantic_v1 from .shared import annotation_is_pydantic_v1 as annotation_is_pydantic_v1
from .shared import field_annotation_is_scalar as field_annotation_is_scalar from .shared import field_annotation_is_scalar as field_annotation_is_scalar
from .shared import is_pydantic_v1_model_class as is_pydantic_v1_model_class
from .shared import is_pydantic_v1_model_instance as is_pydantic_v1_model_instance from .shared import is_pydantic_v1_model_instance as is_pydantic_v1_model_instance
from .shared import ( from .shared import (
is_uploadfile_or_nonable_uploadfile_annotation as is_uploadfile_or_nonable_uploadfile_annotation, is_uploadfile_or_nonable_uploadfile_annotation as is_uploadfile_or_nonable_uploadfile_annotation,
@ -12,21 +11,19 @@ from .shared import (
from .shared import lenient_issubclass as lenient_issubclass from .shared import lenient_issubclass as lenient_issubclass
from .shared import sequence_types as sequence_types from .shared import sequence_types as sequence_types
from .shared import value_is_sequence as value_is_sequence from .shared import value_is_sequence as value_is_sequence
from .v2 import BaseConfig as BaseConfig
from .v2 import ModelField as ModelField from .v2 import ModelField as ModelField
from .v2 import PydanticSchemaGenerationError as PydanticSchemaGenerationError from .v2 import PydanticSchemaGenerationError as PydanticSchemaGenerationError
from .v2 import RequiredParam as RequiredParam from .v2 import RequiredParam as RequiredParam
from .v2 import Undefined as Undefined from .v2 import Undefined as Undefined
from .v2 import UndefinedType as UndefinedType
from .v2 import Url as Url from .v2 import Url as Url
from .v2 import Validator as Validator
from .v2 import _regenerate_error_with_loc as _regenerate_error_with_loc
from .v2 import copy_field_info as copy_field_info from .v2 import copy_field_info as copy_field_info
from .v2 import create_body_model as create_body_model from .v2 import create_body_model as create_body_model
from .v2 import evaluate_forwardref as evaluate_forwardref from .v2 import evaluate_forwardref as evaluate_forwardref
from .v2 import get_cached_model_fields as get_cached_model_fields from .v2 import get_cached_model_fields as get_cached_model_fields
from .v2 import get_definitions as get_definitions from .v2 import get_definitions as get_definitions
from .v2 import get_flat_models_from_fields as get_flat_models_from_fields
from .v2 import get_missing_field_error as get_missing_field_error from .v2 import get_missing_field_error as get_missing_field_error
from .v2 import get_model_name_map as get_model_name_map
from .v2 import get_schema_from_model_field as get_schema_from_model_field from .v2 import get_schema_from_model_field as get_schema_from_model_field
from .v2 import is_bytes_field as is_bytes_field from .v2 import is_bytes_field as is_bytes_field
from .v2 import is_bytes_sequence_field as is_bytes_sequence_field from .v2 import is_bytes_sequence_field as is_bytes_sequence_field

37
fastapi/_compat/shared.py

@ -8,6 +8,7 @@ from dataclasses import is_dataclass
from typing import ( from typing import (
Annotated, Annotated,
Any, Any,
TypeVar,
Union, Union,
) )
@ -15,7 +16,9 @@ from fastapi.types import UnionType
from pydantic import BaseModel from pydantic import BaseModel
from pydantic.version import VERSION as PYDANTIC_VERSION from pydantic.version import VERSION as PYDANTIC_VERSION
from starlette.datastructures import UploadFile from starlette.datastructures import UploadFile
from typing_extensions import get_args, get_origin from typing_extensions import TypeGuard, get_args, get_origin
_T = TypeVar("_T")
# Copy from Pydantic: pydantic/_internal/_typing_extra.py # Copy from Pydantic: pydantic/_internal/_typing_extra.py
if sys.version_info < (3, 10): if sys.version_info < (3, 10):
@ -39,15 +42,13 @@ sequence_annotation_to_type = {
deque: deque, deque: deque,
} }
sequence_types = tuple(sequence_annotation_to_type.keys()) sequence_types: tuple[type[Any], ...] = tuple(sequence_annotation_to_type.keys())
Url: type[Any]
# Copy of Pydantic: pydantic/_internal/_utils.py # Copy of Pydantic: pydantic/_internal/_utils.py with added TypeGuard
def lenient_issubclass( def lenient_issubclass(
cls: Any, class_or_tuple: Union[type[Any], tuple[type[Any], ...], None] cls: Any, class_or_tuple: Union[type[_T], tuple[type[_T], ...], None]
) -> bool: ) -> TypeGuard[type[_T]]:
try: try:
return isinstance(cls, type) and issubclass(cls, class_or_tuple) # type: ignore[arg-type] return isinstance(cls, type) and issubclass(cls, class_or_tuple) # type: ignore[arg-type]
except TypeError: # pragma: no cover except TypeError: # pragma: no cover
@ -177,16 +178,26 @@ def is_uploadfile_sequence_annotation(annotation: Any) -> bool:
def is_pydantic_v1_model_instance(obj: Any) -> bool: def is_pydantic_v1_model_instance(obj: Any) -> bool:
with warnings.catch_warnings(): # TODO: remove this function once the required version of Pydantic fully
warnings.simplefilter("ignore", UserWarning) # removes pydantic.v1
from pydantic import v1 try:
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
from pydantic import v1
except ImportError: # pragma: no cover
return False
return isinstance(obj, v1.BaseModel) return isinstance(obj, v1.BaseModel)
def is_pydantic_v1_model_class(cls: Any) -> bool: def is_pydantic_v1_model_class(cls: Any) -> bool:
with warnings.catch_warnings(): # TODO: remove this function once the required version of Pydantic fully
warnings.simplefilter("ignore", UserWarning) # removes pydantic.v1
from pydantic import v1 try:
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
from pydantic import v1
except ImportError: # pragma: no cover
return False
return lenient_issubclass(cls, v1.BaseModel) return lenient_issubclass(cls, v1.BaseModel)

40
fastapi/_compat/v2.py

@ -12,7 +12,7 @@ from typing import (
cast, cast,
) )
from fastapi._compat import shared from fastapi._compat import lenient_issubclass, shared
from fastapi.openapi.constants import REF_TEMPLATE from fastapi.openapi.constants import REF_TEMPLATE
from fastapi.types import IncEx, ModelNameMap, UnionType from fastapi.types import IncEx, ModelNameMap, UnionType
from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, create_model from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, create_model
@ -23,29 +23,20 @@ from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-
GetJsonSchemaHandler as GetJsonSchemaHandler, GetJsonSchemaHandler as GetJsonSchemaHandler,
) )
from pydantic._internal._typing_extra import eval_type_lenient from pydantic._internal._typing_extra import eval_type_lenient
from pydantic._internal._utils import lenient_issubclass as lenient_issubclass
from pydantic.fields import FieldInfo as FieldInfo from pydantic.fields import FieldInfo as FieldInfo
from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema
from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue
from pydantic_core import CoreSchema as CoreSchema from pydantic_core import CoreSchema as CoreSchema
from pydantic_core import PydanticUndefined, PydanticUndefinedType from pydantic_core import PydanticUndefined
from pydantic_core import Url as Url from pydantic_core import Url as Url
from pydantic_core.core_schema import (
with_info_plain_validator_function as with_info_plain_validator_function,
)
from typing_extensions import Literal, get_args, get_origin from typing_extensions import Literal, get_args, get_origin
try:
from pydantic_core.core_schema import (
with_info_plain_validator_function as with_info_plain_validator_function,
)
except ImportError: # pragma: no cover
from pydantic_core.core_schema import (
general_plain_validator_function as with_info_plain_validator_function, # noqa: F401
)
RequiredParam = PydanticUndefined RequiredParam = PydanticUndefined
Undefined = PydanticUndefined Undefined = PydanticUndefined
UndefinedType = PydanticUndefinedType
evaluate_forwardref = eval_type_lenient evaluate_forwardref = eval_type_lenient
Validator = Any
# TODO: remove when dropping support for Pydantic < v2.12.3 # TODO: remove when dropping support for Pydantic < v2.12.3
_Attrs = { _Attrs = {
@ -87,14 +78,6 @@ def asdict(field_info: FieldInfo) -> dict[str, Any]:
} }
class BaseConfig:
pass
class ErrorWrapper(Exception):
pass
@dataclass @dataclass
class ModelField: class ModelField:
field_info: FieldInfo field_info: FieldInfo
@ -143,8 +126,8 @@ class ModelField:
warnings.simplefilter( warnings.simplefilter(
"ignore", category=UnsupportedFieldAttributeWarning "ignore", category=UnsupportedFieldAttributeWarning
) )
# TODO: remove after dropping support for Python 3.8 and # TODO: remove after setting the min Pydantic to v2.12.3
# setting the min Pydantic to v2.12.3 that adds asdict() # that adds asdict(), and use self.field_info.asdict() instead
field_dict = asdict(self.field_info) field_dict = asdict(self.field_info)
annotated_args = ( annotated_args = (
field_dict["annotation"], field_dict["annotation"],
@ -432,10 +415,11 @@ def get_flat_models_from_annotation(
origin = get_origin(annotation) origin = get_origin(annotation)
if origin is not None: if origin is not None:
for arg in get_args(annotation): for arg in get_args(annotation):
if lenient_issubclass(arg, (BaseModel, Enum)) and arg not in known_models: if lenient_issubclass(arg, (BaseModel, Enum)):
known_models.add(arg) if arg not in known_models:
if lenient_issubclass(arg, BaseModel): known_models.add(arg) # type: ignore[arg-type]
get_flat_models_from_model(arg, known_models=known_models) if lenient_issubclass(arg, BaseModel):
get_flat_models_from_model(arg, known_models=known_models)
else: else:
get_flat_models_from_annotation(arg, known_models=known_models) get_flat_models_from_annotation(arg, known_models=known_models)
return known_models return known_models

8
fastapi/dependencies/utils.py

@ -512,7 +512,6 @@ def analyze_param(
type_=use_annotation_from_field_info, type_=use_annotation_from_field_info,
default=field_info.default, default=field_info.default,
alias=alias, alias=alias,
required=field_info.default in (RequiredParam, Undefined),
field_info=field_info, field_info=field_info,
) )
if is_path_param: if is_path_param:
@ -523,11 +522,7 @@ def analyze_param(
assert ( assert (
is_scalar_field(field) is_scalar_field(field)
or is_scalar_sequence_field(field) or is_scalar_sequence_field(field)
or ( or lenient_issubclass(field.type_, BaseModel)
lenient_issubclass(field.type_, BaseModel)
# For Pydantic v1
and getattr(field, "shape", 1) == 1
)
), f"Query parameter {param_name!r} must be one of the supported types" ), f"Query parameter {param_name!r} must be one of the supported types"
return ParamDetails(type_annotation=type_annotation, depends=depends, field=field) return ParamDetails(type_annotation=type_annotation, depends=depends, field=field)
@ -1021,7 +1016,6 @@ def get_body_field(
final_field = create_model_field( final_field = create_model_field(
name="body", name="body",
type_=BodyModel, type_=BodyModel,
required=required,
alias="body", alias="body",
field_info=BodyFieldInfo(**BodyFieldInfo_kwargs), field_info=BodyFieldInfo(**BodyFieldInfo_kwargs),
) )

5
fastapi/openapi/models.py

@ -143,10 +143,7 @@ class Schema(BaseModelWithConfig):
else_: Optional["SchemaOrBool"] = Field(default=None, alias="else") else_: Optional["SchemaOrBool"] = Field(default=None, alias="else")
dependentSchemas: Optional[dict[str, "SchemaOrBool"]] = None dependentSchemas: Optional[dict[str, "SchemaOrBool"]] = None
prefixItems: Optional[list["SchemaOrBool"]] = None prefixItems: Optional[list["SchemaOrBool"]] = None
# TODO: uncomment and remove below when deprecating Pydantic v1 items: Optional["SchemaOrBool"] = None
# It generates a list of schemas for tuples, before prefixItems was available
# items: Optional["SchemaOrBool"] = None
items: Optional[Union["SchemaOrBool", list["SchemaOrBool"]]] = None
contains: Optional["SchemaOrBool"] = None contains: Optional["SchemaOrBool"] = None
properties: Optional[dict[str, "SchemaOrBool"]] = None properties: Optional[dict[str, "SchemaOrBool"]] = None
patternProperties: Optional[dict[str, "SchemaOrBool"]] = None patternProperties: Optional[dict[str, "SchemaOrBool"]] = None

6
fastapi/openapi/utils.py

@ -10,12 +10,10 @@ from fastapi._compat import (
ModelField, ModelField,
Undefined, Undefined,
get_definitions, get_definitions,
get_schema_from_model_field,
lenient_issubclass,
)
from fastapi._compat.v2 import (
get_flat_models_from_fields, get_flat_models_from_fields,
get_model_name_map, get_model_name_map,
get_schema_from_model_field,
lenient_issubclass,
) )
from fastapi.datastructures import DefaultPlaceholder from fastapi.datastructures import DefaultPlaceholder
from fastapi.dependencies.models import Dependant from fastapi.dependencies.models import Dependant

12
fastapi/routing.py

@ -34,7 +34,6 @@ from fastapi import params
from fastapi._compat import ( from fastapi._compat import (
ModelField, ModelField,
Undefined, Undefined,
annotation_is_pydantic_v1,
lenient_issubclass, lenient_issubclass,
) )
from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.datastructures import Default, DefaultPlaceholder
@ -52,7 +51,6 @@ from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import ( from fastapi.exceptions import (
EndpointContext, EndpointContext,
FastAPIError, FastAPIError,
PydanticV1NotSupportedError,
RequestValidationError, RequestValidationError,
ResponseValidationError, ResponseValidationError,
WebSocketRequestValidationError, WebSocketRequestValidationError,
@ -638,11 +636,6 @@ class APIRoute(routing.Route):
f"Status code {status_code} must not have a response body" f"Status code {status_code} must not have a response body"
) )
response_name = "Response_" + self.unique_id response_name = "Response_" + self.unique_id
if annotation_is_pydantic_v1(self.response_model):
raise PydanticV1NotSupportedError(
"pydantic.v1 models are no longer supported by FastAPI."
f" Please update the response model {self.response_model!r}."
)
self.response_field = create_model_field( self.response_field = create_model_field(
name=response_name, name=response_name,
type_=self.response_model, type_=self.response_model,
@ -664,11 +657,6 @@ class APIRoute(routing.Route):
f"Status code {additional_status_code} must not have a response body" f"Status code {additional_status_code} must not have a response body"
) )
response_name = f"Response_{additional_status_code}_{self.unique_id}" response_name = f"Response_{additional_status_code}_{self.unique_id}"
if annotation_is_pydantic_v1(model):
raise PydanticV1NotSupportedError(
"pydantic.v1 models are no longer supported by FastAPI."
f" In responses={{}}, please update {model}."
)
response_field = create_model_field( response_field = create_model_field(
name=response_name, type_=model, mode="serialization" name=response_name, type_=model, mode="serialization"
) )

12
fastapi/utils.py

@ -9,12 +9,9 @@ from typing import (
import fastapi import fastapi
from fastapi._compat import ( from fastapi._compat import (
BaseConfig,
ModelField, ModelField,
PydanticSchemaGenerationError, PydanticSchemaGenerationError,
Undefined, Undefined,
UndefinedType,
Validator,
annotation_is_pydantic_v1, annotation_is_pydantic_v1,
) )
from fastapi.datastructures import DefaultPlaceholder, DefaultType from fastapi.datastructures import DefaultPlaceholder, DefaultType
@ -63,26 +60,19 @@ _invalid_args_message = (
def create_model_field( def create_model_field(
name: str, name: str,
type_: Any, type_: Any,
class_validators: Optional[dict[str, Validator]] = None,
default: Optional[Any] = Undefined, default: Optional[Any] = Undefined,
required: Union[bool, UndefinedType] = Undefined,
model_config: Union[type[BaseConfig], None] = None,
field_info: Optional[FieldInfo] = None, field_info: Optional[FieldInfo] = None,
alias: Optional[str] = None, alias: Optional[str] = None,
mode: Literal["validation", "serialization"] = "validation", mode: Literal["validation", "serialization"] = "validation",
version: Literal["1", "auto"] = "auto",
) -> ModelField: ) -> ModelField:
if annotation_is_pydantic_v1(type_): if annotation_is_pydantic_v1(type_):
raise PydanticV1NotSupportedError( raise PydanticV1NotSupportedError(
"pydantic.v1 models are no longer supported by FastAPI." "pydantic.v1 models are no longer supported by FastAPI."
f" Please update the response model {type_!r}." f" Please update the response model {type_!r}."
) )
class_validators = class_validators or {}
field_info = field_info or FieldInfo(annotation=type_, default=default, alias=alias) field_info = field_info or FieldInfo(annotation=type_, default=default, alias=alias)
kwargs = {"mode": mode, "name": name, "field_info": field_info}
try: try:
return v2.ModelField(**kwargs) # type: ignore[arg-type] return v2.ModelField(mode=mode, name=name, field_info=field_info)
except PydanticSchemaGenerationError: except PydanticSchemaGenerationError:
raise fastapi.exceptions.FastAPIError( raise fastapi.exceptions.FastAPIError(
_invalid_args_message.format(type_=type_) _invalid_args_message.format(type_=type_)

Loading…
Cancel
Save