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 Annotated, get_args, get_origin # Copy from Pydantic v2, compatible with v1 if sys.version_info < (3, 9): # Pydantic no longer supports Python 3.8, this might be incorrect, but the code # this is used for is also never reached in this codebase, as it's a copy of # Pydantic's lenient_issubclass, just for compatibility with v1 # TODO: remove when dropping support for Python 3.8 WithArgsTypes: Tuple[Any, ...] = () elif sys.version_info < (3, 10): WithArgsTypes: tuple[Any, ...] = (typing._GenericAlias, types.GenericAlias) # type: ignore[attr-defined] else: WithArgsTypes: tuple[Any, ...] = ( typing._GenericAlias, # type: ignore[attr-defined] 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: # pragma: no cover 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) # type: ignore[arg-type] 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)) if origin is Annotated: return field_annotation_is_complex(get_args(annotation)[0]) 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) ) def annotation_is_pydantic_v1(annotation: Any) -> bool: if lenient_issubclass(annotation, 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): return True if field_annotation_is_sequence(annotation): for sub_annotation in get_args(annotation): if annotation_is_pydantic_v1(sub_annotation): return True return False