pythonasyncioapiasyncfastapiframeworkjsonjson-schemaopenapiopenapi3pydanticpython-typespython3redocreststarletteswaggerswagger-uiuvicornweb
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
209 lines
6.8 KiB
209 lines
6.8 KiB
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
|
|
|