Browse Source

🔥 Remove more unnecessary Pydantic v1 specific code

pull/14609/head
Sebastián Ramírez 7 months ago
parent
commit
6fae13125f
  1. 23
      fastapi/_compat/__init__.py
  2. 66
      fastapi/_compat/main.py
  3. 12
      fastapi/_compat/may_v1.py
  4. 30
      fastapi/_compat/v2.py
  5. 22
      fastapi/dependencies/utils.py
  6. 8
      fastapi/encoders.py
  7. 6
      fastapi/openapi/utils.py
  8. 18
      tests/test_compat.py

23
fastapi/_compat/__init__.py

@ -5,25 +5,11 @@ from .main import Undefined as Undefined
from .main import UndefinedType as UndefinedType from .main import UndefinedType as UndefinedType
from .main import Url as Url from .main import Url as Url
from .main import Validator as Validator from .main import Validator as Validator
from .main import _get_model_config as _get_model_config
from .main import _is_model_class as _is_model_class
from .main import _is_model_field as _is_model_field
from .main import _is_undefined as _is_undefined
from .main import _model_dump as _model_dump
from .main import copy_field_info as copy_field_info
from .main import create_body_model as create_body_model
from .main import evaluate_forwardref as evaluate_forwardref from .main import evaluate_forwardref as evaluate_forwardref
from .main import get_annotation_from_field_info as get_annotation_from_field_info
from .main import get_cached_model_fields as get_cached_model_fields from .main import get_cached_model_fields as get_cached_model_fields
from .main import get_compat_model_name_map as get_compat_model_name_map from .main import get_compat_model_name_map as get_compat_model_name_map
from .main import get_missing_field_error as get_missing_field_error from .main import get_missing_field_error as get_missing_field_error
from .main import get_schema_from_model_field as get_schema_from_model_field from .main import get_schema_from_model_field as get_schema_from_model_field
from .main import is_bytes_field as is_bytes_field
from .main import is_bytes_sequence_field as is_bytes_sequence_field
from .main import is_scalar_field as is_scalar_field
from .main import is_scalar_sequence_field as is_scalar_sequence_field
from .main import is_sequence_field as is_sequence_field
from .main import serialize_sequence_value as serialize_sequence_value
from .main import ( from .main import (
with_info_plain_validator_function as with_info_plain_validator_function, with_info_plain_validator_function as with_info_plain_validator_function,
) )
@ -43,4 +29,13 @@ 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 ModelField as ModelField from .v2 import ModelField as ModelField
from .v2 import _regenerate_error_with_loc as _regenerate_error_with_loc
from .v2 import copy_field_info as copy_field_info
from .v2 import create_body_model as create_body_model
from .v2 import get_definitions as get_definitions from .v2 import get_definitions as get_definitions
from .v2 import is_bytes_field as is_bytes_field
from .v2 import is_bytes_sequence_field as is_bytes_sequence_field
from .v2 import is_scalar_field as is_scalar_field
from .v2 import is_scalar_sequence_field as is_scalar_sequence_field
from .v2 import is_sequence_field as is_sequence_field
from .v2 import serialize_sequence_value as serialize_sequence_value

66
fastapi/_compat/main.py

@ -1,10 +1,8 @@
from collections.abc import Sequence
from functools import lru_cache from functools import lru_cache
from typing import ( from typing import (
Any, Any,
) )
from fastapi._compat.shared import lenient_issubclass
from fastapi.types import ModelNameMap from fastapi.types import ModelNameMap
from pydantic import BaseModel from pydantic import BaseModel
from typing_extensions import Literal from typing_extensions import Literal
@ -32,62 +30,6 @@ def get_cached_model_fields(model: type[BaseModel]) -> list[ModelField]:
return get_model_fields(model) # type: ignore[return-value] return get_model_fields(model) # type: ignore[return-value]
def _is_undefined(value: object) -> bool:
return isinstance(value, v2.UndefinedType)
def _get_model_config(model: BaseModel) -> Any:
return v2._get_model_config(model)
def _model_dump(
model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
) -> Any:
return v2._model_dump(model, mode=mode, **kwargs)
def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
return v2.copy_field_info(field_info=field_info, annotation=annotation)
def create_body_model(
*, fields: Sequence[ModelField], model_name: str
) -> type[BaseModel]:
return v2.create_body_model(fields=fields, model_name=model_name) # type: ignore[arg-type]
def get_annotation_from_field_info(
annotation: Any, field_info: FieldInfo, field_name: str
) -> Any:
return v2.get_annotation_from_field_info(
annotation=annotation, field_info=field_info, field_name=field_name
)
def is_bytes_field(field: ModelField) -> bool:
return v2.is_bytes_field(field) # type: ignore[arg-type]
def is_bytes_sequence_field(field: ModelField) -> bool:
return v2.is_bytes_sequence_field(field) # type: ignore[arg-type]
def is_scalar_field(field: ModelField) -> bool:
return v2.is_scalar_field(field) # type: ignore[arg-type]
def is_scalar_sequence_field(field: ModelField) -> bool:
return v2.is_scalar_sequence_field(field) # type: ignore[arg-type]
def is_sequence_field(field: ModelField) -> bool:
return v2.is_sequence_field(field) # type: ignore[arg-type]
def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
return v2.serialize_sequence_value(field=field, value=value) # type: ignore[arg-type]
def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap: def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap:
all_flat_models = set() all_flat_models = set()
@ -115,11 +57,3 @@ def get_schema_from_model_field(
field_mapping=field_mapping, # type: ignore[arg-type] field_mapping=field_mapping, # type: ignore[arg-type]
separate_input_output_schemas=separate_input_output_schemas, separate_input_output_schemas=separate_input_output_schemas,
) )
def _is_model_field(value: Any) -> bool:
return isinstance(value, v2.ModelField)
def _is_model_class(value: Any) -> bool:
return lenient_issubclass(value, v2.BaseModel) # type: ignore[attr-defined]

12
fastapi/_compat/may_v1.py

@ -1,12 +0,0 @@
from collections.abc import Sequence
from typing import Any, Union
def _regenerate_error_with_loc(
*, errors: Sequence[Any], loc_prefix: tuple[Union[str, int], ...]
) -> list[dict[str, Any]]:
updated_loc_errors: list[Any] = [
{**err, "loc": loc_prefix + err.get("loc", ())} for err in errors
]
return updated_loc_errors

30
fastapi/_compat/v2.py

@ -11,7 +11,7 @@ from typing import (
cast, cast,
) )
from fastapi._compat import may_v1, shared from fastapi._compat import 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
@ -175,7 +175,7 @@ class ModelField:
None, None,
) )
except ValidationError as exc: except ValidationError as exc:
return None, may_v1._regenerate_error_with_loc( return None, _regenerate_error_with_loc(
errors=exc.errors(include_url=False), loc_prefix=loc errors=exc.errors(include_url=False), loc_prefix=loc
) )
@ -210,22 +210,6 @@ class ModelField:
return id(self) return id(self)
def get_annotation_from_field_info(
annotation: Any, field_info: FieldInfo, field_name: str
) -> Any:
return annotation
def _model_dump(
model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
) -> Any:
return model.model_dump(mode=mode, **kwargs)
def _get_model_config(model: BaseModel) -> Any:
return model.model_config
def _has_computed_fields(field: ModelField) -> bool: def _has_computed_fields(field: ModelField) -> bool:
computed_fields = field._type_adapter.core_schema.get("schema", {}).get( computed_fields = field._type_adapter.core_schema.get("schema", {}).get(
"computed_fields", [] "computed_fields", []
@ -555,3 +539,13 @@ def get_flat_models_from_fields(
for field in fields: for field in fields:
get_flat_models_from_field(field, known_models=known_models) get_flat_models_from_field(field, known_models=known_models)
return known_models return known_models
def _regenerate_error_with_loc(
*, errors: Sequence[Any], loc_prefix: tuple[Union[str, int], ...]
) -> list[dict[str, Any]]:
updated_loc_errors: list[Any] = [
{**err, "loc": loc_prefix + err.get("loc", ())} for err in errors
]
return updated_loc_errors

22
fastapi/dependencies/utils.py

@ -21,12 +21,11 @@ from fastapi._compat import (
ModelField, ModelField,
RequiredParam, RequiredParam,
Undefined, Undefined,
_is_model_class, _regenerate_error_with_loc,
copy_field_info, copy_field_info,
create_body_model, create_body_model,
evaluate_forwardref, evaluate_forwardref,
field_annotation_is_scalar, field_annotation_is_scalar,
get_annotation_from_field_info,
get_cached_model_fields, get_cached_model_fields,
get_missing_field_error, get_missing_field_error,
is_bytes_field, is_bytes_field,
@ -37,7 +36,6 @@ from fastapi._compat import (
is_uploadfile_or_nonable_uploadfile_annotation, is_uploadfile_or_nonable_uploadfile_annotation,
is_uploadfile_sequence_annotation, is_uploadfile_sequence_annotation,
lenient_issubclass, lenient_issubclass,
may_v1,
sequence_types, sequence_types,
serialize_sequence_value, serialize_sequence_value,
value_is_sequence, value_is_sequence,
@ -184,7 +182,7 @@ def _get_flat_fields_from_params(fields: list[ModelField]) -> list[ModelField]:
if not fields: if not fields:
return fields return fields
first_field = fields[0] first_field = fields[0]
if len(fields) == 1 and _is_model_class(first_field.type_): if len(fields) == 1 and lenient_issubclass(first_field.type_, BaseModel):
fields_to_extract = get_cached_model_fields(first_field.type_) fields_to_extract = get_cached_model_fields(first_field.type_)
return fields_to_extract return fields_to_extract
return fields return fields
@ -492,11 +490,7 @@ def analyze_param(
and getattr(field_info, "in_", None) is None and getattr(field_info, "in_", None) is None
): ):
field_info.in_ = params.ParamTypes.query field_info.in_ = params.ParamTypes.query
use_annotation_from_field_info = get_annotation_from_field_info( use_annotation_from_field_info = use_annotation
use_annotation,
field_info,
param_name,
)
if isinstance(field_info, params.Form): if isinstance(field_info, params.Form):
ensure_multipart_is_installed() ensure_multipart_is_installed()
if not field_info.alias and getattr(field_info, "convert_underscores", None): if not field_info.alias and getattr(field_info, "convert_underscores", None):
@ -521,7 +515,7 @@ def analyze_param(
is_scalar_field(field) is_scalar_field(field)
or is_scalar_sequence_field(field) or is_scalar_sequence_field(field)
or ( or (
_is_model_class(field.type_) lenient_issubclass(field.type_, BaseModel)
# For Pydantic v1 # For Pydantic v1
and getattr(field, "shape", 1) == 1 and getattr(field, "shape", 1) == 1
) )
@ -716,7 +710,7 @@ def _validate_value_with_model_field(
return deepcopy(field.default), [] return deepcopy(field.default), []
v_, errors_ = field.validate(value, values, loc=loc) v_, errors_ = field.validate(value, values, loc=loc)
if isinstance(errors_, list): if isinstance(errors_, list):
new_errors = may_v1._regenerate_error_with_loc(errors=errors_, loc_prefix=()) new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
return None, new_errors return None, new_errors
else: else:
return v_, [] return v_, []
@ -842,7 +836,7 @@ def is_union_of_base_models(field_type: Any) -> bool:
union_args = get_args(field_type) union_args = get_args(field_type)
for arg in union_args: for arg in union_args:
if not _is_model_class(arg): if not lenient_issubclass(arg, BaseModel):
return False return False
return True return True
@ -865,7 +859,7 @@ def _should_embed_body_fields(fields: list[ModelField]) -> bool:
# otherwise it has to be embedded, so that the key value pair can be extracted # otherwise it has to be embedded, so that the key value pair can be extracted
if ( if (
isinstance(first_field.field_info, params.Form) isinstance(first_field.field_info, params.Form)
and not _is_model_class(first_field.type_) and not lenient_issubclass(first_field.type_, BaseModel)
and not is_union_of_base_models(first_field.type_) and not is_union_of_base_models(first_field.type_)
): ):
return True return True
@ -935,7 +929,7 @@ async def request_body_to_args(
if ( if (
single_not_embedded_field single_not_embedded_field
and _is_model_class(first_field.type_) and lenient_issubclass(first_field.type_, BaseModel)
and isinstance(received_body, FormData) and isinstance(received_body, FormData)
): ):
fields_to_extract = get_cached_model_fields(first_field.type_) fields_to_extract = get_cached_model_fields(first_field.type_)

8
fastapi/encoders.py

@ -24,11 +24,10 @@ from pydantic import BaseModel
from pydantic.color import Color from pydantic.color import Color
from pydantic.networks import AnyUrl, NameEmail from pydantic.networks import AnyUrl, NameEmail
from pydantic.types import SecretBytes, SecretStr from pydantic.types import SecretBytes, SecretStr
from pydantic_core import PydanticUndefinedType
from ._compat import ( from ._compat import (
Url, Url,
_is_undefined,
_model_dump,
is_pydantic_v1_model_instance, is_pydantic_v1_model_instance,
) )
@ -224,8 +223,7 @@ def jsonable_encoder(
if exclude is not None and not isinstance(exclude, (set, dict)): if exclude is not None and not isinstance(exclude, (set, dict)):
exclude = set(exclude) exclude = set(exclude)
if isinstance(obj, BaseModel): if isinstance(obj, BaseModel):
obj_dict = _model_dump( obj_dict = obj.model_dump(
obj, # type: ignore[arg-type]
mode="json", mode="json",
include=include, include=include,
exclude=exclude, exclude=exclude,
@ -260,7 +258,7 @@ def jsonable_encoder(
return str(obj) return str(obj)
if isinstance(obj, (str, int, float, type(None))): if isinstance(obj, (str, int, float, type(None))):
return obj return obj
if _is_undefined(obj): if isinstance(obj, PydanticUndefinedType):
return None return None
if isinstance(obj, dict): if isinstance(obj, dict):
encoded_dict = {} encoded_dict = {}

6
fastapi/openapi/utils.py

@ -38,8 +38,6 @@ from starlette.responses import JSONResponse
from starlette.routing import BaseRoute from starlette.routing import BaseRoute
from typing_extensions import Literal from typing_extensions import Literal
from .._compat import _is_model_field
validation_error_definition = { validation_error_definition = {
"title": "ValidationError", "title": "ValidationError",
"type": "object", "type": "object",
@ -187,7 +185,7 @@ def get_openapi_operation_request_body(
) -> Optional[dict[str, Any]]: ) -> Optional[dict[str, Any]]:
if not body_field: if not body_field:
return None return None
assert _is_model_field(body_field) assert isinstance(body_field, ModelField)
body_schema = get_schema_from_model_field( body_schema = get_schema_from_model_field(
field=body_field, field=body_field,
model_name_map=model_name_map, model_name_map=model_name_map,
@ -456,7 +454,7 @@ def get_fields_from_routes(
route, routing.APIRoute route, routing.APIRoute
): ):
if route.body_field: if route.body_field:
assert _is_model_field(route.body_field), ( assert isinstance(route.body_field, ModelField), (
"A request body must be a Pydantic Field" "A request body must be a Pydantic Field"
) )
body_fields_from_routes.append(route.body_field) body_fields_from_routes.append(route.body_field)

18
tests/test_compat.py

@ -3,7 +3,6 @@ from typing import Union
from fastapi import FastAPI, UploadFile from fastapi import FastAPI, UploadFile
from fastapi._compat import ( from fastapi._compat import (
Undefined, Undefined,
_get_model_config,
is_uploadfile_sequence_annotation, is_uploadfile_sequence_annotation,
) )
from fastapi._compat.shared import is_bytes_sequence_annotation from fastapi._compat.shared import is_bytes_sequence_annotation
@ -23,23 +22,6 @@ def test_model_field_default_required():
assert field.default is Undefined assert field.default is Undefined
def test_is_model_field():
# For coverage
from fastapi._compat import _is_model_field
assert not _is_model_field(str)
def test_get_model_config():
# For coverage in Pydantic v2
class Foo(BaseModel):
model_config = ConfigDict(from_attributes=True)
foo = Foo()
config = _get_model_config(foo)
assert config == {"from_attributes": True}
def test_complex(): def test_complex():
app = FastAPI() app = FastAPI()

Loading…
Cancel
Save