diff --git a/fastapi/_compat.py b/fastapi/_compat.py deleted file mode 100644 index 26b6638c8..000000000 --- a/fastapi/_compat.py +++ /dev/null @@ -1,665 +0,0 @@ -from collections import deque -from copy import copy -from dataclasses import dataclass, is_dataclass -from enum import Enum -from functools import lru_cache -from typing import ( - Any, - Callable, - Deque, - Dict, - FrozenSet, - List, - Mapping, - Sequence, - Set, - Tuple, - Type, - Union, - cast, -) - -from fastapi.exceptions import RequestErrorModel -from fastapi.types import IncEx, ModelNameMap, UnionType -from pydantic import BaseModel, create_model -from pydantic.version import VERSION as PYDANTIC_VERSION -from starlette.datastructures import UploadFile -from typing_extensions import Annotated, Literal, get_args, get_origin - -PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2]) -PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2 - - -sequence_annotation_to_type = { - Sequence: list, - List: list, - list: list, - Tuple: tuple, - tuple: tuple, - Set: set, - set: set, - FrozenSet: frozenset, - frozenset: frozenset, - Deque: deque, - deque: deque, -} - -sequence_types = tuple(sequence_annotation_to_type.keys()) - -Url: Type[Any] - -if PYDANTIC_V2: - from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError - from pydantic import TypeAdapter - from pydantic import ValidationError as ValidationError - from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-defined] - GetJsonSchemaHandler as GetJsonSchemaHandler, - ) - from pydantic._internal._typing_extra import eval_type_lenient - from pydantic._internal._utils import lenient_issubclass as lenient_issubclass - from pydantic.fields import FieldInfo - from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema - from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue - from pydantic_core import CoreSchema as CoreSchema - from pydantic_core import PydanticUndefined, PydanticUndefinedType - from pydantic_core import Url as Url - - 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 - Undefined = PydanticUndefined - UndefinedType = PydanticUndefinedType - evaluate_forwardref = eval_type_lenient - Validator = Any - - class BaseConfig: - pass - - class ErrorWrapper(Exception): - pass - - @dataclass - class ModelField: - field_info: FieldInfo - name: str - mode: Literal["validation", "serialization"] = "validation" - - @property - def alias(self) -> str: - a = self.field_info.alias - return a if a is not None else self.name - - @property - def required(self) -> bool: - return self.field_info.is_required() - - @property - def default(self) -> Any: - return self.get_default() - - @property - def type_(self) -> Any: - return self.field_info.annotation - - def __post_init__(self) -> None: - self._type_adapter: TypeAdapter[Any] = TypeAdapter( - Annotated[self.field_info.annotation, self.field_info] - ) - - def get_default(self) -> Any: - if self.field_info.is_required(): - return Undefined - return self.field_info.get_default(call_default_factory=True) - - def validate( - self, - value: Any, - values: Dict[str, Any] = {}, # noqa: B006 - *, - loc: Tuple[Union[int, str], ...] = (), - ) -> Tuple[Any, Union[List[Dict[str, Any]], None]]: - try: - return ( - self._type_adapter.validate_python(value, from_attributes=True), - None, - ) - except ValidationError as exc: - return None, _regenerate_error_with_loc( - errors=exc.errors(include_url=False), loc_prefix=loc - ) - - def serialize( - self, - value: Any, - *, - mode: Literal["json", "python"] = "json", - include: Union[IncEx, None] = None, - exclude: Union[IncEx, None] = None, - by_alias: bool = True, - exclude_unset: bool = False, - exclude_defaults: bool = False, - exclude_none: bool = False, - ) -> Any: - # What calls this code passes a value that already called - # self._type_adapter.validate_python(value) - return self._type_adapter.dump_python( - value, - mode=mode, - include=include, - exclude=exclude, - by_alias=by_alias, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - - def __hash__(self) -> int: - # Each ModelField is unique for our purposes, to allow making a dict from - # ModelField to its JSON Schema. - return id(self) - - def get_annotation_from_field_info( - annotation: Any, field_info: FieldInfo, field_name: str - ) -> Any: - return annotation - - def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]: - return errors # type: ignore[return-value] - - def _model_rebuild(model: Type[BaseModel]) -> None: - model.model_rebuild() - - 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 get_schema_from_model_field( - *, - field: ModelField, - schema_generator: GenerateJsonSchema, - model_name_map: ModelNameMap, - field_mapping: Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ], - separate_input_output_schemas: bool = True, - ) -> Dict[str, Any]: - override_mode: Union[Literal["validation"], None] = ( - None if separate_input_output_schemas else "validation" - ) - # This expects that GenerateJsonSchema was already used to generate the definitions - json_schema = field_mapping[(field, override_mode or field.mode)] - if "$ref" not in json_schema: - # TODO remove when deprecating Pydantic v1 - # Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207 - json_schema["title"] = ( - field.field_info.title or field.alias.title().replace("_", " ") - ) - return json_schema - - def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: - return {} - - def get_definitions( - *, - fields: List[ModelField], - schema_generator: GenerateJsonSchema, - model_name_map: ModelNameMap, - separate_input_output_schemas: bool = True, - ) -> Tuple[ - Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ], - Dict[str, Dict[str, Any]], - ]: - override_mode: Union[Literal["validation"], None] = ( - None if separate_input_output_schemas else "validation" - ) - inputs = [ - (field, override_mode or field.mode, field._type_adapter.core_schema) - for field in fields - ] - field_mapping, definitions = schema_generator.generate_definitions( - inputs=inputs - ) - for item_def in cast(Dict[str, Dict[str, Any]], definitions).values(): - if "description" in item_def: - item_description = cast(str, item_def["description"]).split("\f")[0] - item_def["description"] = item_description - return field_mapping, definitions # type: ignore[return-value] - - def is_scalar_field(field: ModelField) -> bool: - from fastapi import params - - return field_annotation_is_scalar( - field.field_info.annotation - ) and not isinstance(field.field_info, params.Body) - - def is_sequence_field(field: ModelField) -> bool: - return field_annotation_is_sequence(field.field_info.annotation) - - def is_scalar_sequence_field(field: ModelField) -> bool: - return field_annotation_is_scalar_sequence(field.field_info.annotation) - - def is_bytes_field(field: ModelField) -> bool: - return is_bytes_or_nonable_bytes_annotation(field.type_) - - def is_bytes_sequence_field(field: ModelField) -> bool: - return is_bytes_sequence_annotation(field.type_) - - def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: - cls = type(field_info) - merged_field_info = cls.from_annotation(annotation) - new_field_info = copy(field_info) - new_field_info.metadata = merged_field_info.metadata - new_field_info.annotation = merged_field_info.annotation - return new_field_info - - def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: - origin_type = ( - get_origin(field.field_info.annotation) or field.field_info.annotation - ) - assert issubclass(origin_type, sequence_types) # type: ignore[arg-type] - return sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return] - - def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]: - error = ValidationError.from_exception_data( - "Field required", [{"type": "missing", "loc": loc, "input": {}}] - ).errors(include_url=False)[0] - error["input"] = None - return error # type: ignore[return-value] - - def create_body_model( - *, fields: Sequence[ModelField], model_name: str - ) -> Type[BaseModel]: - field_params = {f.name: (f.field_info.annotation, f.field_info) for f in fields} - BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload] - return BodyModel - - def get_model_fields(model: Type[BaseModel]) -> List[ModelField]: - return [ - ModelField(field_info=field_info, name=name) - for name, field_info in model.model_fields.items() - ] - -else: - from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX - from pydantic import AnyUrl as Url # noqa: F401 - from pydantic import ( # type: ignore[assignment] - BaseConfig as BaseConfig, # noqa: F401 - ) - from pydantic import ValidationError as ValidationError # noqa: F401 - from pydantic.class_validators import ( # type: ignore[no-redef] - Validator as Validator, # noqa: F401 - ) - from pydantic.error_wrappers import ( # type: ignore[no-redef] - ErrorWrapper as ErrorWrapper, # noqa: F401 - ) - 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 ( # type: ignore[no-redef,attr-defined] - ModelField as ModelField, # noqa: F401 - ) - - # Keeping old "Required" functionality from Pydantic V1, without - # shadowing typing.Required. - RequiredParam: Any = Ellipsis # type: ignore[no-redef] - from pydantic.fields import ( # type: ignore[no-redef,attr-defined] - Undefined as Undefined, - ) - from pydantic.fields import ( # type: ignore[no-redef, attr-defined] - UndefinedType as UndefinedType, # noqa: F401 - ) - from pydantic.schema import ( - field_schema, - get_flat_models_from_fields, - get_model_name_map, - model_process_schema, - ) - from pydantic.schema import ( # type: ignore[no-redef] # noqa: F401 - get_annotation_from_field_info as get_annotation_from_field_info, - ) - from pydantic.typing import ( # type: ignore[no-redef] - evaluate_forwardref as evaluate_forwardref, # noqa: F401 - ) - from pydantic.utils import ( # type: ignore[no-redef] - lenient_issubclass as lenient_issubclass, # noqa: F401 - ) - - GetJsonSchemaHandler = Any # type: ignore[assignment,misc] - JsonSchemaValue = Dict[str, Any] # type: ignore[misc] - CoreSchema = Any # type: ignore[assignment,misc] - - sequence_shapes = { - SHAPE_LIST, - SHAPE_SET, - SHAPE_FROZENSET, - SHAPE_TUPLE, - SHAPE_SEQUENCE, - SHAPE_TUPLE_ELLIPSIS, - } - sequence_shape_to_type = { - SHAPE_LIST: list, - SHAPE_SET: set, - SHAPE_TUPLE: tuple, - SHAPE_SEQUENCE: list, - SHAPE_TUPLE_ELLIPSIS: list, - } - - @dataclass - class GenerateJsonSchema: # type: ignore[no-redef] - ref_template: str - - class PydanticSchemaGenerationError(Exception): # type: ignore[no-redef] - pass - - def with_info_plain_validator_function( # type: ignore[misc] - function: Callable[..., Any], - *, - ref: Union[str, None] = None, - metadata: Any = None, - serialization: Any = None, - ) -> Any: - return {} - - def get_model_definitions( - *, - flat_models: Set[Union[Type[BaseModel], Type[Enum]]], - model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], - ) -> Dict[str, Any]: - definitions: Dict[str, Dict[str, Any]] = {} - for model in flat_models: - m_schema, m_definitions, m_nested_models = model_process_schema( - model, model_name_map=model_name_map, ref_prefix=REF_PREFIX - ) - definitions.update(m_definitions) - model_name = model_name_map[model] - definitions[model_name] = m_schema - for m_schema in definitions.values(): - if "description" in m_schema: - m_schema["description"] = m_schema["description"].split("\f")[0] - return definitions - - def is_pv1_scalar_field(field: ModelField) -> bool: - from fastapi import params - - field_info = field.field_info - if not ( - field.shape == SHAPE_SINGLETON # type: ignore[attr-defined] - and not lenient_issubclass(field.type_, BaseModel) - and not lenient_issubclass(field.type_, dict) - and not field_annotation_is_sequence(field.type_) - and not is_dataclass(field.type_) - and not isinstance(field_info, params.Body) - ): - return False - if field.sub_fields: # type: ignore[attr-defined] - if not all( - is_pv1_scalar_field(f) - for f in field.sub_fields # type: ignore[attr-defined] - ): - return False - return True - - def is_pv1_scalar_sequence_field(field: ModelField) -> bool: - if (field.shape in sequence_shapes) and not lenient_issubclass( # type: ignore[attr-defined] - field.type_, BaseModel - ): - if field.sub_fields is not None: # type: ignore[attr-defined] - for sub_field in field.sub_fields: # type: ignore[attr-defined] - if not is_pv1_scalar_field(sub_field): - return False - return True - if _annotation_is_sequence(field.type_): - return True - return False - - def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]: - use_errors: List[Any] = [] - for error in errors: - if isinstance(error, ErrorWrapper): - new_errors = ValidationError( # type: ignore[call-arg] - errors=[error], model=RequestErrorModel - ).errors() - use_errors.extend(new_errors) - elif isinstance(error, list): - use_errors.extend(_normalize_errors(error)) - else: - use_errors.append(error) - return use_errors - - def _model_rebuild(model: Type[BaseModel]) -> None: - model.update_forward_refs() - - def _model_dump( - model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any - ) -> Any: - return model.dict(**kwargs) - - def _get_model_config(model: BaseModel) -> Any: - return model.__config__ # type: ignore[attr-defined] - - def get_schema_from_model_field( - *, - field: ModelField, - schema_generator: GenerateJsonSchema, - model_name_map: ModelNameMap, - field_mapping: Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ], - separate_input_output_schemas: bool = True, - ) -> Dict[str, Any]: - # This expects that GenerateJsonSchema was already used to generate the definitions - return field_schema( # type: ignore[no-any-return] - field, model_name_map=model_name_map, ref_prefix=REF_PREFIX - )[0] - - def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: - models = get_flat_models_from_fields(fields, known_models=set()) - return get_model_name_map(models) # type: ignore[no-any-return] - - def get_definitions( - *, - fields: List[ModelField], - schema_generator: GenerateJsonSchema, - model_name_map: ModelNameMap, - separate_input_output_schemas: bool = True, - ) -> Tuple[ - Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ], - Dict[str, Dict[str, Any]], - ]: - models = get_flat_models_from_fields(fields, known_models=set()) - return {}, get_model_definitions( - flat_models=models, model_name_map=model_name_map - ) - - def is_scalar_field(field: ModelField) -> bool: - return is_pv1_scalar_field(field) - - def is_sequence_field(field: ModelField) -> bool: - return field.shape in sequence_shapes or _annotation_is_sequence(field.type_) # type: ignore[attr-defined] - - def is_scalar_sequence_field(field: ModelField) -> bool: - return is_pv1_scalar_sequence_field(field) - - def is_bytes_field(field: ModelField) -> bool: - return lenient_issubclass(field.type_, bytes) - - def is_bytes_sequence_field(field: ModelField) -> bool: - return field.shape in sequence_shapes and lenient_issubclass(field.type_, bytes) # type: ignore[attr-defined] - - def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: - return copy(field_info) - - def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: - return sequence_shape_to_type[field.shape](value) # type: ignore[no-any-return,attr-defined] - - def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]: - missing_field_error = ErrorWrapper(MissingError(), loc=loc) # type: ignore[call-arg] - new_error = ValidationError([missing_field_error], RequestErrorModel) - return new_error.errors()[0] # type: ignore[return-value] - - def create_body_model( - *, fields: Sequence[ModelField], model_name: str - ) -> Type[BaseModel]: - BodyModel = create_model(model_name) - for f in fields: - BodyModel.__fields__[f.name] = f # type: ignore[index] - return BodyModel - - def get_model_fields(model: Type[BaseModel]) -> List[ModelField]: - return list(model.__fields__.values()) # type: ignore[attr-defined] - - -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 _normalize_errors(errors) - ] - - return updated_loc_errors - - -def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: - if lenient_issubclass(annotation, (str, bytes)): - return False - return lenient_issubclass(annotation, sequence_types) - - -def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: - origin = get_origin(annotation) - if origin is Union or origin is UnionType: - for arg in get_args(annotation): - if field_annotation_is_sequence(arg): - return True - return False - return _annotation_is_sequence(annotation) or _annotation_is_sequence( - get_origin(annotation) - ) - - -def value_is_sequence(value: Any) -> bool: - return isinstance(value, sequence_types) and not isinstance(value, (str, bytes)) # type: ignore[arg-type] - - -def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool: - return ( - lenient_issubclass(annotation, (BaseModel, Mapping, UploadFile)) - or _annotation_is_sequence(annotation) - or is_dataclass(annotation) - ) - - -def field_annotation_is_complex(annotation: Union[Type[Any], None]) -> bool: - origin = get_origin(annotation) - if origin is Union or origin is UnionType: - return any(field_annotation_is_complex(arg) for arg in get_args(annotation)) - - return ( - _annotation_is_complex(annotation) - or _annotation_is_complex(origin) - or hasattr(origin, "__pydantic_core_schema__") - or hasattr(origin, "__get_pydantic_core_schema__") - ) - - -def field_annotation_is_scalar(annotation: Any) -> bool: - # handle Ellipsis here to make tuple[int, ...] work nicely - return annotation is Ellipsis or not field_annotation_is_complex(annotation) - - -def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> bool: - origin = get_origin(annotation) - if origin is Union or origin is UnionType: - at_least_one_scalar_sequence = False - for arg in get_args(annotation): - if field_annotation_is_scalar_sequence(arg): - at_least_one_scalar_sequence = True - continue - elif not field_annotation_is_scalar(arg): - return False - return at_least_one_scalar_sequence - return field_annotation_is_sequence(annotation) and all( - field_annotation_is_scalar(sub_annotation) - for sub_annotation in get_args(annotation) - ) - - -def is_bytes_or_nonable_bytes_annotation(annotation: Any) -> bool: - if lenient_issubclass(annotation, bytes): - return True - origin = get_origin(annotation) - if origin is Union or origin is UnionType: - for arg in get_args(annotation): - if lenient_issubclass(arg, bytes): - return True - return False - - -def is_uploadfile_or_nonable_uploadfile_annotation(annotation: Any) -> bool: - if lenient_issubclass(annotation, UploadFile): - return True - origin = get_origin(annotation) - if origin is Union or origin is UnionType: - for arg in get_args(annotation): - if lenient_issubclass(arg, UploadFile): - return True - return False - - -def is_bytes_sequence_annotation(annotation: Any) -> bool: - origin = get_origin(annotation) - if origin is Union or origin is UnionType: - at_least_one = False - for arg in get_args(annotation): - if is_bytes_sequence_annotation(arg): - at_least_one = True - continue - return at_least_one - return field_annotation_is_sequence(annotation) and all( - is_bytes_or_nonable_bytes_annotation(sub_annotation) - for sub_annotation in get_args(annotation) - ) - - -def is_uploadfile_sequence_annotation(annotation: Any) -> bool: - origin = get_origin(annotation) - if origin is Union or origin is UnionType: - at_least_one = False - for arg in get_args(annotation): - if is_uploadfile_sequence_annotation(arg): - at_least_one = True - continue - return at_least_one - return field_annotation_is_sequence(annotation) and all( - is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation) - for sub_annotation in get_args(annotation) - ) - - -@lru_cache -def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]: - return get_model_fields(model) diff --git a/fastapi/_compat/__init__.py b/fastapi/_compat/__init__.py new file mode 100644 index 000000000..5d772e583 --- /dev/null +++ b/fastapi/_compat/__init__.py @@ -0,0 +1,49 @@ +from .main import BaseConfig as BaseConfig +from .main import PydanticSchemaGenerationError as PydanticSchemaGenerationError +from .main import RequiredParam as RequiredParam +from .main import Undefined as Undefined +from .main import UndefinedType as UndefinedType +from .main import Url as Url +from .main import Validator as Validator +from .main import _get_model_config as _get_model_config +from .main import _is_error_wrapper as _is_error_wrapper +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 _model_rebuild as _model_rebuild +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 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_compat_model_name_map as get_compat_model_name_map +from .main import get_definitions as get_definitions +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 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 ( + with_info_plain_validator_function as with_info_plain_validator_function, +) +from .model_field import ModelField as ModelField +from .shared import PYDANTIC_V2 as PYDANTIC_V2 +from .shared import PYDANTIC_VERSION_MINOR_TUPLE as PYDANTIC_VERSION_MINOR_TUPLE +from .shared import field_annotation_is_scalar as field_annotation_is_scalar +from .shared import ( + is_uploadfile_or_nonable_uploadfile_annotation as is_uploadfile_or_nonable_uploadfile_annotation, +) +from .shared import ( + is_uploadfile_sequence_annotation as is_uploadfile_sequence_annotation, +) +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 diff --git a/fastapi/_compat/main.py b/fastapi/_compat/main.py new file mode 100644 index 000000000..64fe21177 --- /dev/null +++ b/fastapi/_compat/main.py @@ -0,0 +1,299 @@ +from functools import lru_cache +from typing import ( + Any, + Dict, + List, + Sequence, + Tuple, + Type, +) + +from fastapi._compat import v1 +from fastapi._compat.shared import lenient_issubclass +from fastapi.types import ModelNameMap +from fastapi.x_compat import PYDANTIC_V2 +from pydantic import BaseModel +from typing_extensions import Literal + +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 + from .v1 import FieldInfo as FieldInfo + from .v1 import 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 + 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 ( + with_info_plain_validator_function as with_info_plain_validator_function, + ) + + +@lru_cache +def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]: + if lenient_issubclass(model, v1.BaseModel): + return v1.get_model_fields(model) # type: ignore[return-value] + else: + from . import v2 + + return v2.get_model_fields(model) # type: ignore[return-value] + + +def _is_undefined(value: object) -> bool: + if isinstance(value, v1.UndefinedType): + return True + elif PYDANTIC_V2: + from . import v2 + + return isinstance(value, v2.UndefinedType) + return False + + +def _get_model_config(model: BaseModel) -> Any: + if lenient_issubclass(model, v1.BaseModel): + return v1._get_model_config(model) + elif PYDANTIC_V2: + from . import v2 + + return v2._get_model_config(model) + + +def _model_dump( + model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any +) -> Any: + if lenient_issubclass(model, v1.BaseModel): + return v1._model_dump(model, mode=mode, **kwargs) + elif PYDANTIC_V2: + from . import v2 + + return v2._model_dump(model, mode=mode, **kwargs) + + +def _is_error_wrapper(exc: Exception) -> bool: + if isinstance(exc, v1.ErrorWrapper): + return True + elif PYDANTIC_V2: + from . import v2 + + return isinstance(exc, v2.ErrorWrapper) + return False + + +def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: + if isinstance(field_info, v1.FieldInfo): + return v1.copy_field_info(field_info=field_info, annotation=annotation) + elif PYDANTIC_V2: + from . import v2 + + return v2.copy_field_info(field_info=field_info, annotation=annotation) + raise TypeError("field_info must be an instance of FieldInfo") + + +def create_body_model( + *, fields: Sequence[ModelField], model_name: str +) -> Type[BaseModel]: + if fields and isinstance(fields[0], v1.ModelField): + return v1.create_body_model(fields=fields, model_name=model_name) # type: ignore[return-value] + elif PYDANTIC_V2: + from . import v2 + + return v2.create_body_model(fields=fields, model_name=model_name) # type: ignore[return-value] + raise TypeError("fields must be a sequence of ModelField instances") + + +def get_annotation_from_field_info( + annotation: Any, field_info: FieldInfo, field_name: str +) -> Any: + if isinstance(field_info, v1.FieldInfo): + return v1.get_annotation_from_field_info( + annotation=annotation, field_info=field_info, field_name=field_name + ) + elif PYDANTIC_V2: + from . import v2 + + return v2.get_annotation_from_field_info( + annotation=annotation, field_info=field_info, field_name=field_name + ) + raise TypeError("field_info must be an instance of FieldInfo") + + +def is_bytes_field(field: ModelField) -> bool: + if isinstance(field, v1.ModelField): + return v1.is_bytes_field(field) + elif PYDANTIC_V2: + from . import v2 + + return v2.is_bytes_field(field) # type: ignore[return-value] + raise TypeError("field_info must be an instance of FieldInfo") + + +def is_bytes_sequence_field(field: ModelField) -> bool: + if isinstance(field, v1.ModelField): + return v1.is_bytes_sequence_field(field) + elif PYDANTIC_V2: + from . import v2 + + return v2.is_bytes_sequence_field(field) # type: ignore[return-value] + raise TypeError("field_info must be an instance of FieldInfo") + + +def is_scalar_field(field: ModelField) -> bool: + if isinstance(field, v1.ModelField): + return v1.is_scalar_field(field) + elif PYDANTIC_V2: + from . import v2 + + return v2.is_scalar_field(field) # type: ignore[return-value] + raise TypeError("field_info must be an instance of FieldInfo") + + +def is_scalar_sequence_field(field: ModelField) -> bool: + if isinstance(field, v1.ModelField): + return v1.is_scalar_sequence_field(field) + elif PYDANTIC_V2: + from . import v2 + + return v2.is_scalar_sequence_field(field) # type: ignore[return-value] + raise TypeError("field_info must be an instance of FieldInfo") + + +def is_sequence_field(field: ModelField) -> bool: + if isinstance(field, v1.ModelField): + return v1.is_sequence_field(field) + elif PYDANTIC_V2: + from . import v2 + + return v2.is_sequence_field(field) # type: ignore[return-value] + raise TypeError("field_info must be an instance of FieldInfo") + + +def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: + if isinstance(field, v1.ModelField): + return v1.serialize_sequence_value(field=field, value=value) + elif PYDANTIC_V2: + from . import v2 + + return v2.serialize_sequence_value(field=field, value=value) # type: ignore[return-value] + raise TypeError("field_info must be an instance of FieldInfo") + + +def _model_rebuild(model: Type[BaseModel]) -> None: + if lenient_issubclass(model, v1.BaseModel): + v1._model_rebuild(model) + elif PYDANTIC_V2: + from . import v2 + + v2._model_rebuild(model) + + +def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: + models = v1.get_flat_models_from_fields(fields, known_models=set()) + return v1.get_model_name_map(models) # type: ignore[no-any-return] + + +def get_definitions( + *, + fields: List[ModelField], + model_name_map: ModelNameMap, + separate_input_output_schemas: bool = True, +) -> Tuple[ + Dict[Tuple[ModelField, Literal["validation", "serialization"]], 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( + fields=v1_fields, + 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, + ) + # TODO: check for uniqueness + all_definitions = {**v1_definitions, **v2_definitions} + all_field_maps = {**v1_field_maps, **v2_field_maps} + return all_field_maps, all_definitions + + +def get_schema_from_model_field( + *, + field: ModelField, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], v1.JsonSchemaValue + ], + separate_input_output_schemas: bool = True, +) -> Dict[str, Any]: + if isinstance(field, v1.ModelField): + return v1.get_schema_from_model_field( + field=field, + model_name_map=model_name_map, + field_mapping=field_mapping, # type: ignore[arg-type] + separate_input_output_schemas=separate_input_output_schemas, + ) + elif PYDANTIC_V2: + from . import v2 + + return v2.get_schema_from_model_field( + field=field, + model_name_map=model_name_map, + field_mapping=field_mapping, # type: ignore[arg-type] + separate_input_output_schemas=separate_input_output_schemas, + ) + raise TypeError("field must be an instance of ModelField") + + +def _is_model_field(value: Any) -> bool: + if isinstance(value, v1.ModelField): + return True + elif PYDANTIC_V2: + from . import v2 + + return isinstance(value, v2.ModelField) + return False + + +def _is_field_info(value: Any) -> bool: + if isinstance(value, v1.FieldInfo): + return True + elif PYDANTIC_V2: + from . import v2 + + return isinstance(value, v2.FieldInfo) + return False + + +def _is_model_class(value: Any) -> bool: + if lenient_issubclass(value, v1.BaseModel): + return True + elif PYDANTIC_V2: + from . import v2 + + return lenient_issubclass(value, v2.BaseModel) + return False diff --git a/fastapi/_compat/model_field.py b/fastapi/_compat/model_field.py new file mode 100644 index 000000000..0fa9a3dfa --- /dev/null +++ b/fastapi/_compat/model_field.py @@ -0,0 +1,91 @@ +from dataclasses import dataclass +from typing import ( + Any, + Dict, + List, + Tuple, + Union, +) + +from fastapi.types import IncEx +from pydantic.fields import FieldInfo +from typing_extensions import Literal + + +@dataclass +class ModelField: + field_info: "FieldInfo" + name: str + mode: Literal["validation", "serialization"] = "validation" + _version: Literal["v1", "v2"] = "v1" + + @property + def alias(self) -> str: + return self._model_field.alias + + @property + def required(self) -> bool: + return self._model_field.required + + @property + def default(self) -> Any: + return self._model_field.default + + @property + def type_(self) -> Any: + return self._model_field.type_ + + def __post_init__(self) -> None: + if self._version == "v1": + from . import v1 + + self._model_field = v1.ModelField( + field_info=self.field_info, name=self.name + ) + else: + assert self._version == "v2" + from . import v2 + + self._model_field = v2.ModelField( + field_info=self.field_info, name=self.name, mode=self.mode + ) + + def get_default(self) -> Any: + return self._model_field.get_default() + + def validate( + self, + value: Any, + values: Dict[str, Any] = {}, # noqa: B006 + *, + loc: Tuple[Union[int, str], ...] = (), + ) -> Tuple[Any, Union[List[Dict[str, Any]], None]]: + return self._model_field.validate(value=value, values=values, loc=loc) + + def serialize( + self, + value: Any, + *, + mode: Literal["json", "python"] = "json", + include: Union[IncEx, None] = None, + exclude: Union[IncEx, None] = None, + by_alias: bool = True, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + ) -> Any: + return self._model_field.serialize( + value=value, + mode=mode, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + def __hash__(self) -> int: + # Each ModelField is unique for our purposes, to allow making a dict from + # ModelField to its JSON Schema. + return id(self) diff --git a/fastapi/_compat/shared.py b/fastapi/_compat/shared.py new file mode 100644 index 000000000..56e4ce46c --- /dev/null +++ b/fastapi/_compat/shared.py @@ -0,0 +1,185 @@ +import sys +import types +import typing +from collections import deque +from dataclasses import is_dataclass +from typing import ( + Any, + Deque, + FrozenSet, + List, + Mapping, + Sequence, + Set, + Tuple, + Type, + Union, +) + +from fastapi._compat import v1 +from fastapi.types import UnionType +from pydantic import BaseModel +from pydantic.version import VERSION as PYDANTIC_VERSION +from starlette.datastructures import UploadFile +from typing_extensions import get_args, get_origin + +# Copy from Pydantic v2, compatible with v1 +if sys.version_info < (3, 10): + WithArgsTypes: tuple[Any, ...] = (typing._GenericAlias, types.GenericAlias) # pyright: ignore[reportAttributeAccessIssue] +else: + WithArgsTypes: tuple[Any, ...] = ( + typing._GenericAlias, + types.GenericAlias, + types.UnionType, + ) # pyright: ignore[reportAttributeAccessIssue] + +PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2]) +PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2 + + +sequence_annotation_to_type = { + Sequence: list, + List: list, + list: list, + Tuple: tuple, + tuple: tuple, + Set: set, + set: set, + FrozenSet: frozenset, + frozenset: frozenset, + Deque: deque, + deque: deque, +} + +sequence_types = tuple(sequence_annotation_to_type.keys()) + +Url: Type[Any] + + +# Copy of Pydantic v2, compatible with v1 +def lenient_issubclass( + cls: Any, class_or_tuple: Union[Type[Any], Tuple[Type[Any], ...], None] +) -> bool: + try: + return isinstance(cls, type) and issubclass(cls, class_or_tuple) # type: ignore[arg-type] + except TypeError: + if isinstance(cls, WithArgsTypes): + return False + raise # pragma: no cover + + +def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: + if lenient_issubclass(annotation, (str, bytes)): + return False + return lenient_issubclass(annotation, sequence_types) + + +def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + for arg in get_args(annotation): + if field_annotation_is_sequence(arg): + return True + return False + return _annotation_is_sequence(annotation) or _annotation_is_sequence( + get_origin(annotation) + ) + + +def value_is_sequence(value: Any) -> bool: + return isinstance(value, sequence_types) and not isinstance(value, (str, bytes)) # type: ignore[arg-type] + + +def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool: + return ( + lenient_issubclass(annotation, (BaseModel, v1.BaseModel, Mapping, UploadFile)) + or _annotation_is_sequence(annotation) + or is_dataclass(annotation) + ) + + +def field_annotation_is_complex(annotation: Union[Type[Any], None]) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + return any(field_annotation_is_complex(arg) for arg in get_args(annotation)) + + return ( + _annotation_is_complex(annotation) + or _annotation_is_complex(origin) + or hasattr(origin, "__pydantic_core_schema__") + or hasattr(origin, "__get_pydantic_core_schema__") + ) + + +def field_annotation_is_scalar(annotation: Any) -> bool: + # handle Ellipsis here to make tuple[int, ...] work nicely + return annotation is Ellipsis or not field_annotation_is_complex(annotation) + + +def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + at_least_one_scalar_sequence = False + for arg in get_args(annotation): + if field_annotation_is_scalar_sequence(arg): + at_least_one_scalar_sequence = True + continue + elif not field_annotation_is_scalar(arg): + return False + return at_least_one_scalar_sequence + return field_annotation_is_sequence(annotation) and all( + field_annotation_is_scalar(sub_annotation) + for sub_annotation in get_args(annotation) + ) + + +def is_bytes_or_nonable_bytes_annotation(annotation: Any) -> bool: + if lenient_issubclass(annotation, bytes): + return True + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + for arg in get_args(annotation): + if lenient_issubclass(arg, bytes): + return True + return False + + +def is_uploadfile_or_nonable_uploadfile_annotation(annotation: Any) -> bool: + if lenient_issubclass(annotation, UploadFile): + return True + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + for arg in get_args(annotation): + if lenient_issubclass(arg, UploadFile): + return True + return False + + +def is_bytes_sequence_annotation(annotation: Any) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + at_least_one = False + for arg in get_args(annotation): + if is_bytes_sequence_annotation(arg): + at_least_one = True + continue + return at_least_one + return field_annotation_is_sequence(annotation) and all( + is_bytes_or_nonable_bytes_annotation(sub_annotation) + for sub_annotation in get_args(annotation) + ) + + +def is_uploadfile_sequence_annotation(annotation: Any) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + at_least_one = False + for arg in get_args(annotation): + if is_uploadfile_sequence_annotation(arg): + at_least_one = True + continue + return at_least_one + return field_annotation_is_sequence(annotation) and all( + is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation) + for sub_annotation in get_args(annotation) + ) diff --git a/fastapi/_compat/v1.py b/fastapi/_compat/v1.py new file mode 100644 index 000000000..9023c3220 --- /dev/null +++ b/fastapi/_compat/v1.py @@ -0,0 +1,354 @@ +from copy import copy +from dataclasses import dataclass, is_dataclass +from enum import Enum +from typing import ( + Any, + Callable, + Dict, + List, + Sequence, + Set, + Tuple, + Type, + Union, +) + +from fastapi._compat import shared +from fastapi.exceptions import RequestErrorModel +from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX +from fastapi.types import ModelNameMap +from pydantic.version import VERSION as PYDANTIC_VERSION +from typing_extensions import Literal + +PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2]) +PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2 +# Keeping old "Required" functionality from Pydantic V1, without +# shadowing typing.Required. +RequiredParam: Any = Ellipsis # type: ignore[no-redef] + +if not PYDANTIC_V2: + from pydantic import AnyUrl as Url # noqa: F401 + from pydantic import ( # type: ignore[assignment] + BaseConfig as BaseConfig, # noqa: F401 + ) + from pydantic import BaseModel, create_model + from pydantic import ValidationError as ValidationError # noqa: F401 + from pydantic.class_validators import ( # type: ignore[no-redef] + Validator as Validator, # noqa: F401 + ) + from pydantic.color import Color as Color # noqa: F401 + from pydantic.error_wrappers import ( # type: ignore[no-redef] + ErrorWrapper as ErrorWrapper, # noqa: F401 + ) + 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 ( # type: ignore[no-redef,attr-defined] + ModelField as ModelField, # noqa: F401 + ) + from pydantic.fields import ( # type: ignore[no-redef,attr-defined] + Undefined as Undefined, + ) + from pydantic.fields import ( # type: ignore[no-redef, attr-defined] + UndefinedType as UndefinedType, # noqa: F401 + ) + from pydantic.networks import AnyUrl as AnyUrl # noqa: F401 + from pydantic.networks import NameEmail as NameEmail # noqa: F401 + from pydantic.schema import TypeModelSet as TypeModelSet + from pydantic.schema import ( + field_schema, + get_flat_models_from_fields, + model_process_schema, + ) + from pydantic.schema import ( # type: ignore[no-redef] # noqa: F401 + 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.types import SecretBytes as SecretBytes # noqa: F401 + from pydantic.types import SecretStr as SecretStr # noqa: F401 + from pydantic.typing import ( # type: ignore[no-redef] + evaluate_forwardref as evaluate_forwardref, # noqa: F401 + ) + from pydantic.utils import ( # type: ignore[no-redef] + lenient_issubclass as lenient_issubclass, # noqa: F401 + ) + +else: + from pydantic.v1 import AnyUrl as Url # noqa: F401 + from pydantic.v1 import ( # type: ignore[assignment] + BaseConfig as BaseConfig, # noqa: F401 + ) + from pydantic.v1 import BaseModel, create_model + from pydantic.v1 import ValidationError as ValidationError # noqa: F401 + from pydantic.v1.class_validators import ( # type: ignore[no-redef] + Validator as Validator, # noqa: F401 + ) + from pydantic.v1.color import Color as Color # noqa: F401 + from pydantic.v1.error_wrappers import ( # type: ignore[no-redef] + ErrorWrapper as ErrorWrapper, # noqa: F401 + ) + from pydantic.v1.errors import MissingError + from pydantic.v1.fields import ( # type: ignore[attr-defined] + SHAPE_FROZENSET, + SHAPE_LIST, + SHAPE_SEQUENCE, + SHAPE_SET, + SHAPE_SINGLETON, + SHAPE_TUPLE, + SHAPE_TUPLE_ELLIPSIS, + ) + from pydantic.v1.fields import FieldInfo as FieldInfo + from pydantic.v1.fields import ( # type: ignore[no-redef,attr-defined] + ModelField as ModelField, # noqa: F401 + ) + from pydantic.v1.fields import ( # type: ignore[no-redef,attr-defined] + Undefined as Undefined, + ) + from pydantic.v1.fields import ( # type: ignore[no-redef, attr-defined] + UndefinedType as UndefinedType, # noqa: F401 + ) + from pydantic.v1.networks import AnyUrl as AnyUrl # noqa: F401 + from pydantic.v1.networks import NameEmail as NameEmail # noqa: F401 + from pydantic.v1.schema import TypeModelSet as TypeModelSet + from pydantic.v1.schema import ( + field_schema, + get_flat_models_from_fields, + model_process_schema, + ) + from pydantic.v1.schema import ( # type: ignore[no-redef] # noqa: F401 + 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, # noqa: F401 + ) + from pydantic.v1.types import SecretBytes as SecretBytes # noqa: F401 + from pydantic.v1.types import SecretStr as SecretStr # noqa: F401 + from pydantic.v1.typing import ( # type: ignore[no-redef] + evaluate_forwardref as evaluate_forwardref, # noqa: F401 + ) + from pydantic.v1.utils import ( # type: ignore[no-redef] + lenient_issubclass as lenient_issubclass, # noqa: F401 + ) + + +GetJsonSchemaHandler = Any # type: ignore[assignment,misc] +JsonSchemaValue = Dict[str, Any] # type: ignore[misc] +CoreSchema = Any # type: ignore[assignment,misc] + +sequence_shapes = { + SHAPE_LIST, + SHAPE_SET, + SHAPE_FROZENSET, + SHAPE_TUPLE, + SHAPE_SEQUENCE, + SHAPE_TUPLE_ELLIPSIS, +} +sequence_shape_to_type = { + SHAPE_LIST: list, + SHAPE_SET: set, + SHAPE_TUPLE: tuple, + SHAPE_SEQUENCE: list, + SHAPE_TUPLE_ELLIPSIS: list, +} + + +@dataclass +class GenerateJsonSchema: # type: ignore[no-redef] + ref_template: str + + +class PydanticSchemaGenerationError(Exception): # type: ignore[no-redef] + pass + + +def with_info_plain_validator_function( # type: ignore[misc] + function: Callable[..., Any], + *, + ref: Union[str, None] = None, + metadata: Any = None, + serialization: Any = None, +) -> Any: + return {} + + +def get_model_definitions( + *, + flat_models: Set[Union[Type[BaseModel], Type[Enum]]], + model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], +) -> Dict[str, Any]: + definitions: Dict[str, Dict[str, Any]] = {} + for model in flat_models: + m_schema, m_definitions, m_nested_models = model_process_schema( + model, model_name_map=model_name_map, ref_prefix=REF_PREFIX + ) + definitions.update(m_definitions) + model_name = model_name_map[model] + definitions[model_name] = m_schema + for m_schema in definitions.values(): + if "description" in m_schema: + m_schema["description"] = m_schema["description"].split("\f")[0] + return definitions + + +def is_pv1_scalar_field(field: ModelField) -> bool: + from fastapi import params + + field_info = field.field_info + if not ( + field.shape == SHAPE_SINGLETON # type: ignore[attr-defined] + and not lenient_issubclass(field.type_, BaseModel) + and not lenient_issubclass(field.type_, dict) + and not shared.field_annotation_is_sequence(field.type_) + and not is_dataclass(field.type_) + and not isinstance(field_info, params.Body) + ): + return False + if field.sub_fields: # type: ignore[attr-defined] + if not all( + is_pv1_scalar_field(f) + for f in field.sub_fields # type: ignore[attr-defined] + ): + return False + return True + + +def is_pv1_scalar_sequence_field(field: ModelField) -> bool: + if (field.shape in sequence_shapes) and not lenient_issubclass( # type: ignore[attr-defined] + field.type_, BaseModel + ): + if field.sub_fields is not None: # type: ignore[attr-defined] + for sub_field in field.sub_fields: # type: ignore[attr-defined] + if not is_pv1_scalar_field(sub_field): + return False + return True + if shared._annotation_is_sequence(field.type_): + return True + return False + + +def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]: + use_errors: List[Any] = [] + for error in errors: + if isinstance(error, ErrorWrapper): + new_errors = ValidationError( # type: ignore[call-arg] + errors=[error], model=RequestErrorModel + ).errors() + use_errors.extend(new_errors) + elif isinstance(error, list): + use_errors.extend(_normalize_errors(error)) + else: + use_errors.append(error) + return use_errors + + +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 _normalize_errors(errors) + ] + + return updated_loc_errors + + +def _model_rebuild(model: Type[BaseModel]) -> None: + model.update_forward_refs() + + +def _model_dump( + model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any +) -> Any: + return model.dict(**kwargs) + + +def _get_model_config(model: BaseModel) -> Any: + return model.__config__ # type: ignore[attr-defined] + + +def get_schema_from_model_field( + *, + field: ModelField, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + separate_input_output_schemas: bool = True, +) -> Dict[str, Any]: + # This expects that GenerateJsonSchema was already used to generate the definitions + return field_schema( # type: ignore[no-any-return] + field, model_name_map=model_name_map, ref_prefix=REF_PREFIX + )[0] + + +# def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: +# models = get_flat_models_from_fields(fields, known_models=set()) +# return get_model_name_map(models) # type: ignore[no-any-return] + + +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]], +]: + models = get_flat_models_from_fields(fields, known_models=set()) + return {}, get_model_definitions(flat_models=models, model_name_map=model_name_map) + + +def is_scalar_field(field: ModelField) -> bool: + return is_pv1_scalar_field(field) + + +def is_sequence_field(field: ModelField) -> bool: + return field.shape in sequence_shapes or shared._annotation_is_sequence(field.type_) # type: ignore[attr-defined] + + +def is_scalar_sequence_field(field: ModelField) -> bool: + return is_pv1_scalar_sequence_field(field) + + +def is_bytes_field(field: ModelField) -> bool: + return lenient_issubclass(field.type_, bytes) + + +def is_bytes_sequence_field(field: ModelField) -> bool: + return field.shape in sequence_shapes and lenient_issubclass(field.type_, bytes) # type: ignore[attr-defined] + + +def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: + return copy(field_info) + + +def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: + return sequence_shape_to_type[field.shape](value) # type: ignore[no-any-return,attr-defined] + + +def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]: + missing_field_error = ErrorWrapper(MissingError(), loc=loc) # type: ignore[call-arg] + new_error = ValidationError([missing_field_error], RequestErrorModel) + return new_error.errors()[0] # type: ignore[return-value] + + +def create_body_model( + *, fields: Sequence[ModelField], model_name: str +) -> Type[BaseModel]: + BodyModel = create_model(model_name) + for f in fields: + BodyModel.__fields__[f.name] = f # type: ignore[index] + return BodyModel + + +def get_model_fields(model: Type[BaseModel]) -> List[ModelField]: + return list(model.__fields__.values()) # type: ignore[attr-defined] diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py new file mode 100644 index 000000000..943aa0eb6 --- /dev/null +++ b/fastapi/_compat/v2.py @@ -0,0 +1,346 @@ +from copy import copy, deepcopy +from dataclasses import dataclass +from typing import ( + Any, + Dict, + List, + Sequence, + Tuple, + Type, + Union, + cast, +) + +from fastapi._compat import shared, v1 +from fastapi.openapi.constants import REF_TEMPLATE +from fastapi.types import IncEx, ModelNameMap +from pydantic import BaseModel, TypeAdapter, create_model +from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError +from pydantic import PydanticUndefinedAnnotation as PydanticUndefinedAnnotation +from pydantic import ValidationError as ValidationError +from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-defined] + GetJsonSchemaHandler as GetJsonSchemaHandler, +) +from pydantic._internal._typing_extra import eval_type_lenient +from pydantic._internal._utils import lenient_issubclass as lenient_issubclass +from pydantic.fields import FieldInfo +from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema +from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue +from pydantic_core import CoreSchema as CoreSchema +from pydantic_core import PydanticUndefined, PydanticUndefinedType +from pydantic_core import Url as Url +from typing_extensions import Annotated, Literal, 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 +Undefined = PydanticUndefined +UndefinedType = PydanticUndefinedType +evaluate_forwardref = eval_type_lenient +Validator = Any + + +class BaseConfig: + pass + + +class ErrorWrapper(Exception): + pass + + +@dataclass +class ModelField: + field_info: FieldInfo + name: str + mode: Literal["validation", "serialization"] = "validation" + + @property + def alias(self) -> str: + a = self.field_info.alias + return a if a is not None else self.name + + @property + def required(self) -> bool: + return self.field_info.is_required() + + @property + def default(self) -> Any: + return self.get_default() + + @property + def type_(self) -> Any: + return self.field_info.annotation + + def __post_init__(self) -> None: + self._type_adapter: TypeAdapter[Any] = TypeAdapter( + Annotated[self.field_info.annotation, self.field_info] + ) + + def get_default(self) -> Any: + if self.field_info.is_required(): + return Undefined + return self.field_info.get_default(call_default_factory=True) + + def validate( + self, + value: Any, + values: Dict[str, Any] = {}, # noqa: B006 + *, + loc: Tuple[Union[int, str], ...] = (), + ) -> Tuple[Any, Union[List[Dict[str, Any]], None]]: + try: + return ( + self._type_adapter.validate_python(value, from_attributes=True), + None, + ) + except ValidationError as exc: + return None, v1._regenerate_error_with_loc( + errors=exc.errors(include_url=False), loc_prefix=loc + ) + + def serialize( + self, + value: Any, + *, + mode: Literal["json", "python"] = "json", + include: Union[IncEx, None] = None, + exclude: Union[IncEx, None] = None, + by_alias: bool = True, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + ) -> Any: + # What calls this code passes a value that already called + # self._type_adapter.validate_python(value) + return self._type_adapter.dump_python( + value, + mode=mode, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + def __hash__(self) -> int: + # Each ModelField is unique for our purposes, to allow making a dict from + # ModelField to its JSON Schema. + return id(self) + + +def get_annotation_from_field_info( + annotation: Any, field_info: FieldInfo, field_name: str +) -> Any: + return annotation + + +def _model_rebuild(model: Type[BaseModel]) -> None: + model.model_rebuild() + + +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 get_schema_from_model_field( + *, + field: ModelField, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + separate_input_output_schemas: bool = True, +) -> Dict[str, Any]: + override_mode: Union[Literal["validation"], None] = ( + None if separate_input_output_schemas else "validation" + ) + # This expects that GenerateJsonSchema was already used to generate the definitions + json_schema = field_mapping[(field, override_mode or field.mode)] + if "$ref" not in json_schema: + # TODO remove when deprecating Pydantic v1 + # Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207 + json_schema["title"] = field.field_info.title or field.alias.title().replace( + "_", " " + ) + return json_schema + + +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]], +]: + schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE) + override_mode: Union[Literal["validation"], None] = ( + None if separate_input_output_schemas else "validation" + ) + inputs = [ + (field, override_mode or field.mode, field._type_adapter.core_schema) + for field in fields + ] + field_mapping, definitions = schema_generator.generate_definitions(inputs=inputs) + for item_def in cast(Dict[str, Dict[str, Any]], definitions).values(): + if "description" in item_def: + item_description = cast(str, item_def["description"]).split("\f")[0] + item_def["description"] = item_description + new_mapping, new_definitions = _remap_definitions_and_field_mappings( + model_name_map=model_name_map, + definitions=definitions, # type: ignore[arg-type] + field_mapping=field_mapping, + ) + return new_mapping, new_definitions + + +def _replace_refs( + *, + schema: Dict[str, Any], + old_name_to_new_name_map: Dict[str, str], +) -> Dict[str, Any]: + new_schema = deepcopy(schema) + for key, value in new_schema.items(): + if key == "$ref": + ref_name = schema["$ref"].split("/")[-1] + if ref_name in old_name_to_new_name_map: + new_name = old_name_to_new_name_map[ref_name] + new_schema["$ref"] = REF_TEMPLATE.format(new_name) + else: + new_schema["$ref"] = schema["$ref"] + continue + if isinstance(value, dict): + new_schema[key] = _replace_refs( + schema=value, + old_name_to_new_name_map=old_name_to_new_name_map, + ) + elif isinstance(value, list): + new_value = [] + for item in value: + if isinstance(item, dict): + new_item = _replace_refs( + schema=item, + old_name_to_new_name_map=old_name_to_new_name_map, + ) + new_value.append(new_item) + + else: + new_value.append(item) + new_schema[key] = new_value + return new_schema + + +def _remap_definitions_and_field_mappings( + *, + model_name_map: ModelNameMap, + definitions: Dict[str, Any], + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], +): + old_name_to_new_name_map = {} + for key, value in field_mapping.items(): + model = key[0].type_ + if model not in model_name_map: + continue + new_name = model_name_map[model] + old_name = value["$ref"].split("/")[-1] + old_name_to_new_name_map[old_name] = new_name + + new_field_mapping = {} + for key, value in field_mapping.items(): + new_value = _replace_refs( + schema=value, + old_name_to_new_name_map=old_name_to_new_name_map, + ) + new_field_mapping[key] = new_value + + new_definitions = {} + for key, value in definitions.items(): + if key in old_name_to_new_name_map: + new_key = old_name_to_new_name_map[key] + else: + new_key = key + new_value = _replace_refs( + schema=value, + old_name_to_new_name_map=old_name_to_new_name_map, + ) + new_definitions[new_key] = new_value + return new_field_mapping, new_definitions + + +def is_scalar_field(field: ModelField) -> bool: + from fastapi import params + + return shared.field_annotation_is_scalar( + field.field_info.annotation + ) and not isinstance(field.field_info, params.Body) + + +def is_sequence_field(field: ModelField) -> bool: + return shared.field_annotation_is_sequence(field.field_info.annotation) + + +def is_scalar_sequence_field(field: ModelField) -> bool: + return shared.field_annotation_is_scalar_sequence(field.field_info.annotation) + + +def is_bytes_field(field: ModelField) -> bool: + return shared.is_bytes_or_nonable_bytes_annotation(field.type_) + + +def is_bytes_sequence_field(field: ModelField) -> bool: + return shared.is_bytes_sequence_annotation(field.type_) + + +def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: + cls = type(field_info) + merged_field_info = cls.from_annotation(annotation) + new_field_info = copy(field_info) + new_field_info.metadata = merged_field_info.metadata + new_field_info.annotation = merged_field_info.annotation + return new_field_info + + +def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: + origin_type = get_origin(field.field_info.annotation) or field.field_info.annotation + assert issubclass(origin_type, shared.sequence_types) # type: ignore[arg-type] + return shared.sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return] + + +def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]: + error = ValidationError.from_exception_data( + "Field required", [{"type": "missing", "loc": loc, "input": {}}] + ).errors(include_url=False)[0] + error["input"] = None + return error # type: ignore[return-value] + + +def create_body_model( + *, fields: Sequence[ModelField], model_name: str +) -> Type[BaseModel]: + field_params = {f.name: (f.field_info.annotation, f.field_info) for f in fields} + BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload] + return BodyModel + + +def get_model_fields(model: Type[BaseModel]) -> List[ModelField]: + return [ + ModelField(field_info=field_info, name=name) + for name, field_info in model.model_fields.items() + ]