Browse Source

introduce may_v1 to introduce dummies for Python 3.14

pull/14186/head
svlandeg 9 months ago
parent
commit
eb43072f09
  1. 8
      fastapi/_compat/__init__.py
  2. 71
      fastapi/_compat/main.py
  3. 104
      fastapi/_compat/may_v1.py
  4. 8
      fastapi/_compat/shared.py
  5. 4
      fastapi/_compat/v2.py
  6. 18
      fastapi/dependencies/utils.py
  7. 18
      fastapi/encoders.py
  8. 2
      fastapi/temp_pydantic_v1_params.py
  9. 12
      fastapi/utils.py
  10. 10
      tests/test_compat.py
  11. 2
      tests/test_get_model_definitions_formfeed_escape.py
  12. 5
      tests/test_response_model_as_return_annotation.py

8
fastapi/_compat/__init__.py

@ -44,7 +44,7 @@ from .shared import (
from .shared import lenient_issubclass as lenient_issubclass
from .shared import sequence_types as sequence_types
from .shared import value_is_sequence as value_is_sequence
from .v1 import CoreSchema as CoreSchema
from .v1 import GetJsonSchemaHandler as GetJsonSchemaHandler
from .v1 import JsonSchemaValue as JsonSchemaValue
from .v1 import _normalize_errors as _normalize_errors
from .may_v1 import CoreSchema as CoreSchema
from .may_v1 import GetJsonSchemaHandler as GetJsonSchemaHandler
from .may_v1 import JsonSchemaValue as JsonSchemaValue
from .may_v1 import _normalize_errors as _normalize_errors

71
fastapi/_compat/main.py

@ -8,7 +8,7 @@ from typing import (
Type,
)
from fastapi._compat import v1
from fastapi._compat import may_v1
from fastapi._compat.shared import PYDANTIC_V2, lenient_issubclass
from fastapi.types import ModelNameMap
from pydantic import BaseModel
@ -50,7 +50,8 @@ else:
@lru_cache
def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]:
if lenient_issubclass(model, v1.BaseModel):
if lenient_issubclass(model, may_v1.BaseModel):
from fastapi._compat import v1
return v1.get_model_fields(model)
else:
from . import v2
@ -59,7 +60,8 @@ def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]:
def _is_undefined(value: object) -> bool:
if isinstance(value, v1.UndefinedType):
if isinstance(value, may_v1.UndefinedType):
from fastapi._compat import v1
return True
elif PYDANTIC_V2:
from . import v2
@ -69,7 +71,8 @@ def _is_undefined(value: object) -> bool:
def _get_model_config(model: BaseModel) -> Any:
if isinstance(model, v1.BaseModel):
if isinstance(model, may_v1.BaseModel):
from fastapi._compat import v1
return v1._get_model_config(model)
elif PYDANTIC_V2:
from . import v2
@ -80,7 +83,8 @@ def _get_model_config(model: BaseModel) -> Any:
def _model_dump(
model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
) -> Any:
if isinstance(model, v1.BaseModel):
if isinstance(model, may_v1.BaseModel):
from fastapi._compat import v1
return v1._model_dump(model, mode=mode, **kwargs)
elif PYDANTIC_V2:
from . import v2
@ -89,7 +93,7 @@ def _model_dump(
def _is_error_wrapper(exc: Exception) -> bool:
if isinstance(exc, v1.ErrorWrapper):
if isinstance(exc, may_v1.ErrorWrapper):
return True
elif PYDANTIC_V2:
from . import v2
@ -99,7 +103,8 @@ def _is_error_wrapper(exc: Exception) -> bool:
def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
if isinstance(field_info, v1.FieldInfo):
if isinstance(field_info, may_v1.FieldInfo):
from fastapi._compat import v1
return v1.copy_field_info(field_info=field_info, annotation=annotation)
else:
assert PYDANTIC_V2
@ -111,7 +116,8 @@ def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
def create_body_model(
*, fields: Sequence[ModelField], model_name: str
) -> Type[BaseModel]:
if fields and isinstance(fields[0], v1.ModelField):
if fields and isinstance(fields[0], may_v1.ModelField):
from fastapi._compat import v1
return v1.create_body_model(fields=fields, model_name=model_name)
else:
assert PYDANTIC_V2
@ -123,7 +129,8 @@ def create_body_model(
def get_annotation_from_field_info(
annotation: Any, field_info: FieldInfo, field_name: str
) -> Any:
if isinstance(field_info, v1.FieldInfo):
if isinstance(field_info, may_v1.FieldInfo):
from fastapi._compat import v1
return v1.get_annotation_from_field_info(
annotation=annotation, field_info=field_info, field_name=field_name
)
@ -137,7 +144,8 @@ def get_annotation_from_field_info(
def is_bytes_field(field: ModelField) -> bool:
if isinstance(field, v1.ModelField):
if isinstance(field, may_v1.ModelField):
from fastapi._compat import v1
return v1.is_bytes_field(field)
else:
assert PYDANTIC_V2
@ -147,7 +155,8 @@ def is_bytes_field(field: ModelField) -> bool:
def is_bytes_sequence_field(field: ModelField) -> bool:
if isinstance(field, v1.ModelField):
if isinstance(field, may_v1.ModelField):
from fastapi._compat import v1
return v1.is_bytes_sequence_field(field)
else:
assert PYDANTIC_V2
@ -157,7 +166,8 @@ def is_bytes_sequence_field(field: ModelField) -> bool:
def is_scalar_field(field: ModelField) -> bool:
if isinstance(field, v1.ModelField):
if isinstance(field, may_v1.ModelField):
from fastapi._compat import v1
return v1.is_scalar_field(field)
else:
assert PYDANTIC_V2
@ -167,7 +177,8 @@ def is_scalar_field(field: ModelField) -> bool:
def is_scalar_sequence_field(field: ModelField) -> bool:
if isinstance(field, v1.ModelField):
if isinstance(field, may_v1.ModelField):
from fastapi._compat import v1
return v1.is_scalar_sequence_field(field)
else:
assert PYDANTIC_V2
@ -177,7 +188,8 @@ def is_scalar_sequence_field(field: ModelField) -> bool:
def is_sequence_field(field: ModelField) -> bool:
if isinstance(field, v1.ModelField):
if isinstance(field, may_v1.ModelField):
from fastapi._compat import v1
return v1.is_sequence_field(field)
else:
assert PYDANTIC_V2
@ -187,7 +199,8 @@ def is_sequence_field(field: ModelField) -> bool:
def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
if isinstance(field, v1.ModelField):
if isinstance(field, may_v1.ModelField):
from fastapi._compat import v1
return v1.serialize_sequence_value(field=field, value=value)
else:
assert PYDANTIC_V2
@ -197,7 +210,8 @@ def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
def _model_rebuild(model: Type[BaseModel]) -> None:
if lenient_issubclass(model, v1.BaseModel):
if lenient_issubclass(model, may_v1.BaseModel):
from fastapi._compat import v1
v1._model_rebuild(model)
elif PYDANTIC_V2:
from . import v2
@ -206,9 +220,13 @@ def _model_rebuild(model: Type[BaseModel]) -> None:
def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
v1_model_fields = [field for field in fields if isinstance(field, v1.ModelField)]
v1_flat_models = v1.get_flat_models_from_fields(v1_model_fields, known_models=set()) # type: ignore[attr-defined]
all_flat_models = v1_flat_models
v1_model_fields = [field for field in fields if isinstance(field, may_v1.ModelField)]
if len(v1_model_fields) > 0:
from fastapi._compat import v1
v1_flat_models = v1.get_flat_models_from_fields(v1_model_fields, known_models=set()) # type: ignore[attr-defined]
all_flat_models = v1_flat_models
else:
all_flat_models = set()
if PYDANTIC_V2:
from . import v2
@ -222,6 +240,7 @@ def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
model_name_map = v2.get_model_name_map(all_flat_models)
return model_name_map
from fastapi._compat import v1
model_name_map = v1.get_model_name_map(all_flat_models)
return model_name_map
@ -232,11 +251,11 @@ def get_definitions(
model_name_map: ModelNameMap,
separate_input_output_schemas: bool = True,
) -> Tuple[
Dict[Tuple[ModelField, Literal["validation", "serialization"]], v1.JsonSchemaValue],
Dict[Tuple[ModelField, Literal["validation", "serialization"]], may_v1.JsonSchemaValue],
Dict[str, Dict[str, Any]],
]:
v1_fields = [field for field in fields if isinstance(field, v1.ModelField)]
v1_field_maps, v1_definitions = v1.get_definitions(
v1_fields = [field for field in fields if isinstance(field, may_v1.ModelField)]
v1_field_maps, v1_definitions = may_v1.get_definitions(
fields=v1_fields,
model_name_map=model_name_map,
separate_input_output_schemas=separate_input_output_schemas,
@ -266,7 +285,9 @@ def get_schema_from_model_field(
],
separate_input_output_schemas: bool = True,
) -> Dict[str, Any]:
if isinstance(field, v1.ModelField):
if isinstance(field, may_v1.ModelField):
from fastapi._compat import v1
return v1.get_schema_from_model_field(
field=field,
model_name_map=model_name_map,
@ -286,7 +307,7 @@ def get_schema_from_model_field(
def _is_model_field(value: Any) -> bool:
if isinstance(value, v1.ModelField):
if isinstance(value, may_v1.ModelField):
return True
elif PYDANTIC_V2:
from . import v2
@ -296,7 +317,7 @@ def _is_model_field(value: Any) -> bool:
def _is_model_class(value: Any) -> bool:
if lenient_issubclass(value, v1.BaseModel):
if lenient_issubclass(value, may_v1.BaseModel):
return True
elif PYDANTIC_V2:
from . import v2

104
fastapi/_compat/may_v1.py

@ -0,0 +1,104 @@
import sys
from typing import Any, Dict, List, Literal, Sequence, Tuple, Union
from fastapi.types import ModelNameMap
if sys.version_info >= (3, 14):
class BaseConfig():
pass
class FieldInfo():
pass
class BaseModel():
pass
class UndefinedType():
pass
class ErrorWrapper():
pass
class ModelField():
pass
class JsonSchemaValue():
pass
class Color():
pass
class NameEmail():
pass
class SecretBytes():
pass
class SecretStr():
pass
class AnyUrl():
pass
class Url():
pass
class CoreSchema():
pass
class GetJsonSchemaHandler():
pass
class JsonSchemaValue():
pass
class Undefined():
pass
class RequiredParam():
pass
def get_definitions(
*,
fields: List[ModelField],
model_name_map: ModelNameMap,
separate_input_output_schemas: bool = True,
) -> Tuple[
Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue],
Dict[str, Dict[str, Any]],
]:
return {}, {}
def _regenerate_error_with_loc(
*, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...]
) -> List[Dict[str, Any]]:
return []
def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
return []
else:
from fastapi._compat import v1
from .v1 import BaseConfig as BaseConfig # type: ignore[assignment]
from .v1 import FieldInfo as FieldInfo
from .v1 import BaseModel
from .v1 import UndefinedType as UndefinedType
from .v1 import ErrorWrapper
from .v1 import ModelField
from .v1 import JsonSchemaValue
from .v1 import Color
from .v1 import NameEmail
from .v1 import SecretBytes
from .v1 import SecretStr
from .v1 import AnyUrl
from .v1 import Url
from .v1 import get_definitions
from .v1 import _regenerate_error_with_loc
from .v1 import CoreSchema as CoreSchema
from .v1 import GetJsonSchemaHandler as GetJsonSchemaHandler
from .v1 import JsonSchemaValue as JsonSchemaValue
from .v1 import _normalize_errors as _normalize_errors
from .v1 import Undefined
from .v1 import RequiredParam

8
fastapi/_compat/shared.py

@ -16,7 +16,7 @@ from typing import (
Union,
)
from fastapi._compat import v1
from fastapi._compat import may_v1
from fastapi.types import UnionType
from pydantic import BaseModel
from pydantic.version import VERSION as PYDANTIC_VERSION
@ -98,7 +98,7 @@ def value_is_sequence(value: Any) -> bool:
def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool:
return (
lenient_issubclass(annotation, (BaseModel, v1.BaseModel, Mapping, UploadFile))
lenient_issubclass(annotation, (BaseModel, may_v1.BaseModel, Mapping, UploadFile))
or _annotation_is_sequence(annotation)
or is_dataclass(annotation)
)
@ -195,12 +195,12 @@ def is_uploadfile_sequence_annotation(annotation: Any) -> bool:
def annotation_is_pydantic_v1(annotation: Any) -> bool:
if lenient_issubclass(annotation, v1.BaseModel):
if lenient_issubclass(annotation, may_v1.BaseModel):
return True
origin = get_origin(annotation)
if origin is Union or origin is UnionType:
for arg in get_args(annotation):
if lenient_issubclass(arg, v1.BaseModel):
if lenient_issubclass(arg, may_v1.BaseModel):
return True
if field_annotation_is_sequence(annotation):
for sub_annotation in get_args(annotation):

4
fastapi/_compat/v2.py

@ -15,7 +15,7 @@ from typing import (
cast,
)
from fastapi._compat import shared, v1
from fastapi._compat import shared, may_v1
from fastapi.openapi.constants import REF_TEMPLATE
from fastapi.types import IncEx, ModelNameMap
from pydantic import BaseModel, TypeAdapter, create_model
@ -116,7 +116,7 @@ class ModelField:
None,
)
except ValidationError as exc:
return None, v1._regenerate_error_with_loc(
return None, may_v1._regenerate_error_with_loc(
errors=exc.errors(include_url=False), loc_prefix=loc
)

18
fastapi/dependencies/utils.py

@ -45,7 +45,7 @@ from fastapi._compat import (
lenient_issubclass,
sequence_types,
serialize_sequence_value,
v1,
may_v1,
value_is_sequence,
)
from fastapi._compat.shared import annotation_is_pydantic_v1
@ -380,7 +380,7 @@ def analyze_param(
fastapi_annotations = [
arg
for arg in annotated_args[1:]
if isinstance(arg, (FieldInfo, v1.FieldInfo, params.Depends))
if isinstance(arg, (FieldInfo, may_v1.FieldInfo, params.Depends))
]
fastapi_specific_annotations = [
arg
@ -397,21 +397,21 @@ def analyze_param(
)
]
if fastapi_specific_annotations:
fastapi_annotation: Union[FieldInfo, v1.FieldInfo, params.Depends, None] = (
fastapi_annotation: Union[FieldInfo, may_v1.FieldInfo, params.Depends, None] = (
fastapi_specific_annotations[-1]
)
else:
fastapi_annotation = None
# Set default for Annotated FieldInfo
if isinstance(fastapi_annotation, (FieldInfo, v1.FieldInfo)):
if isinstance(fastapi_annotation, (FieldInfo, may_v1.FieldInfo)):
# Copy `field_info` because we mutate `field_info.default` below.
field_info = copy_field_info(
field_info=fastapi_annotation, annotation=use_annotation
)
assert field_info.default in {
Undefined,
v1.Undefined,
} or field_info.default in {RequiredParam, v1.RequiredParam}, (
may_v1.Undefined,
} or field_info.default in {RequiredParam, may_v1.RequiredParam}, (
f"`{field_info.__class__.__name__}` default value cannot be set in"
f" `Annotated` for {param_name!r}. Set the default value with `=` instead."
)
@ -435,7 +435,7 @@ def analyze_param(
)
depends = value
# Get FieldInfo from default value
elif isinstance(value, (FieldInfo, v1.FieldInfo)):
elif isinstance(value, (FieldInfo, may_v1.FieldInfo)):
assert field_info is None, (
"Cannot specify FastAPI annotations in `Annotated` and default value"
f" together for {param_name!r}"
@ -524,7 +524,7 @@ def analyze_param(
type_=use_annotation_from_field_info,
default=field_info.default,
alias=alias,
required=field_info.default in (RequiredParam, v1.RequiredParam, Undefined),
required=field_info.default in (RequiredParam, may_v1.RequiredParam, Undefined),
field_info=field_info,
)
if is_path_param:
@ -741,7 +741,7 @@ def _validate_value_with_model_field(
if _is_error_wrapper(errors_): # type: ignore[arg-type]
return None, [errors_]
elif isinstance(errors_, list):
new_errors = v1._regenerate_error_with_loc(errors=errors_, loc_prefix=())
new_errors = may_v1._regenerate_error_with_loc(errors=errors_, loc_prefix=())
return None, new_errors
else:
return v_, []

18
fastapi/encoders.py

@ -17,7 +17,7 @@ from types import GeneratorType
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
from uuid import UUID
from fastapi._compat import v1
from fastapi._compat import may_v1
from fastapi.types import IncEx
from pydantic import BaseModel
from pydantic.color import Color
@ -59,7 +59,7 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = {
bytes: lambda o: o.decode(),
Color: str,
v1.Color: str,
may_v1.Color: str,
datetime.date: isoformat,
datetime.datetime: isoformat,
datetime.time: isoformat,
@ -76,19 +76,19 @@ ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = {
IPv6Interface: str,
IPv6Network: str,
NameEmail: str,
v1.NameEmail: str,
may_v1.NameEmail: str,
Path: str,
Pattern: lambda o: o.pattern,
SecretBytes: str,
v1.SecretBytes: str,
may_v1.SecretBytes: str,
SecretStr: str,
v1.SecretStr: str,
may_v1.SecretStr: str,
set: list,
UUID: str,
Url: str,
v1.Url: str,
may_v1.Url: str,
AnyUrl: str,
v1.AnyUrl: str,
may_v1.AnyUrl: str,
}
@ -220,10 +220,10 @@ def jsonable_encoder(
include = set(include)
if exclude is not None and not isinstance(exclude, (set, dict)):
exclude = set(exclude)
if isinstance(obj, (BaseModel, v1.BaseModel)):
if isinstance(obj, (BaseModel, may_v1.BaseModel)):
# TODO: remove when deprecating Pydantic v1
encoders: Dict[Any, Any] = {}
if isinstance(obj, v1.BaseModel):
if isinstance(obj, may_v1.BaseModel):
encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined]
if custom_encoder:
encoders = {**encoders, **custom_encoder}

2
fastapi/temp_pydantic_v1_params.py

@ -6,7 +6,7 @@ from fastapi.params import ParamTypes
from typing_extensions import Annotated, deprecated
from ._compat.shared import PYDANTIC_VERSION_MINOR_TUPLE
from ._compat.v1 import FieldInfo, Undefined
from ._compat.may_v1 import FieldInfo, Undefined
_Unset: Any = Undefined

12
fastapi/utils.py

@ -25,7 +25,7 @@ from fastapi._compat import (
Validator,
annotation_is_pydantic_v1,
lenient_issubclass,
v1,
may_v1
)
from fastapi.datastructures import DefaultPlaceholder, DefaultType
from pydantic import BaseModel
@ -87,8 +87,8 @@ def create_model_field(
) -> ModelField:
class_validators = class_validators or {}
v1_model_config = v1.BaseConfig
v1_field_info = field_info or v1.FieldInfo()
v1_model_config = may_v1.BaseConfig
v1_field_info = field_info or may_v1.FieldInfo()
v1_kwargs = {
"name": name,
"field_info": v1_field_info,
@ -102,9 +102,10 @@ def create_model_field(
if (
annotation_is_pydantic_v1(type_)
or isinstance(field_info, v1.FieldInfo)
or isinstance(field_info, may_v1.FieldInfo)
or version == "1"
):
from fastapi._compat import v1
try:
return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return]
except RuntimeError:
@ -122,6 +123,7 @@ def create_model_field(
raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None
# Pydantic v2 is not installed, but it's not a Pydantic v1 ModelField, it could be
# a Pydantic v1 type, like a constrained int
from fastapi._compat import v1
try:
return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return]
except RuntimeError:
@ -138,6 +140,8 @@ def create_cloned_field(
if isinstance(field, v2.ModelField):
return field
from fastapi._compat import v1
# cloned_types caches already cloned types to support recursive models and improve
# performance by avoiding unnecessary cloning
if cloned_types is None:

10
tests/test_compat.py

@ -7,7 +7,7 @@ from fastapi._compat import (
get_cached_model_fields,
is_scalar_field,
is_uploadfile_sequence_annotation,
v1,
may_v1,
)
from fastapi._compat.shared import is_bytes_sequence_annotation
from fastapi.testclient import TestClient
@ -27,7 +27,10 @@ def test_model_field_default_required():
assert field.default is Undefined
@needs_py_lt_314
def test_v1_plain_validator_function():
from fastapi._compat import v1
# For coverage
def func(v): # pragma: no cover
return v
@ -135,6 +138,7 @@ def test_is_uploadfile_sequence_annotation():
@needs_py_lt_314
def test_is_pv1_scalar_field():
from fastapi._compat import v1
# For coverage
class Model(v1.BaseModel):
foo: Union[str, Dict[str, Any]]
@ -143,8 +147,10 @@ def test_is_pv1_scalar_field():
assert not is_scalar_field(fields[0])
@needs_py_lt_314
def test_get_model_fields_cached():
class Model(v1.BaseModel):
from fastapi._compat import v1
class Model(may_v1.BaseModel):
foo: str
non_cached_fields = v1.get_model_fields(Model)

2
tests/test_get_model_definitions_formfeed_escape.py

@ -5,7 +5,6 @@ import fastapi.openapi.utils
import pydantic.schema
import pytest
from fastapi import FastAPI
from fastapi._compat import v1
from pydantic import BaseModel
from starlette.testclient import TestClient
@ -165,6 +164,7 @@ def test_model_description_escaped_with_formfeed(sort_reversed: bool):
Test `get_model_definitions` with models passed in different order.
"""
from fastapi._compat import v1
all_fields = fastapi.openapi.utils.get_fields_from_routes(app.routes)
flat_models = v1.get_flat_models_from_fields(all_fields, known_models=set())

5
tests/test_response_model_as_return_annotation.py

@ -2,12 +2,13 @@ from typing import List, Union
import pytest
from fastapi import FastAPI
from fastapi._compat import v1
from fastapi.exceptions import FastAPIError, ResponseValidationError
from fastapi.responses import JSONResponse, Response
from fastapi.testclient import TestClient
from pydantic import BaseModel
from tests.utils import needs_pydanticv1
class BaseUser(BaseModel):
name: str
@ -511,7 +512,9 @@ def test_invalid_response_model_field():
# TODO: remove when dropping Pydantic v1 support
@needs_pydanticv1
def test_invalid_response_model_field_pv1():
from fastapi._compat import v1
app = FastAPI()
class Model(v1.BaseModel):

Loading…
Cancel
Save