fastapi/_compat/v2.py PYTHON 494 lines View on github.com → Search inside
1import re2import warnings3from collections.abc import Sequence4from copy import copy5from dataclasses import dataclass, is_dataclass6from enum import Enum7from functools import lru_cache8from typing import (9    Annotated,10    Any,11    Literal,12    Union,13    cast,14    get_args,15    get_origin,16)1718from fastapi._compat import lenient_issubclass, shared19from fastapi.openapi.constants import REF_TEMPLATE20from fastapi.types import IncEx, ModelNameMap, UnionType21from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, create_model22from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError23from pydantic import PydanticUndefinedAnnotation as PydanticUndefinedAnnotation24from pydantic import ValidationError as ValidationError25from pydantic._internal import _typing_extra as _pydantic_typing_extra26from pydantic._internal._schema_generation_shared import (  # type: ignore[attr-defined]27    GetJsonSchemaHandler as GetJsonSchemaHandler,28)29from pydantic.fields import FieldInfo as FieldInfo30from pydantic.json_schema import GenerateJsonSchema as _GenerateJsonSchema31from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue32from pydantic_core import CoreSchema as CoreSchema33from pydantic_core import PydanticUndefined34from pydantic_core import Url as Url35from pydantic_core.core_schema import (36    with_info_plain_validator_function as with_info_plain_validator_function,37)3839RequiredParam = PydanticUndefined40Undefined = PydanticUndefined414243def evaluate_forwardref(44    value: Any,45    globalns: dict[str, Any] | None = None,46    localns: dict[str, Any] | None = None,47) -> Any:48    # eval_type_lenient has been deprecated since Pydantic v2.10.0b1 (PR #10530)49    try_eval_type = getattr(_pydantic_typing_extra, "try_eval_type", None)50    if try_eval_type is not None:51        return try_eval_type(value, globalns, localns)[0]52    return _pydantic_typing_extra.eval_type_lenient(  # ty: ignore[deprecated]53        value, globalns, localns54    )555657class GenerateJsonSchema(_GenerateJsonSchema):58    # TODO: remove when this is merged (or equivalent): https://github.com/pydantic/pydantic/pull/1284159    # and dropping support for any version of Pydantic before that one (so, in a very long time)60    def bytes_schema(self, schema: CoreSchema) -> JsonSchemaValue:61        json_schema = {"type": "string", "contentMediaType": "application/octet-stream"}62        bytes_mode = (63            self._config.ser_json_bytes64            if self.mode == "serialization"65            else self._config.val_json_bytes66        )67        if bytes_mode == "base64":68            json_schema["contentEncoding"] = "base64"69        self.update_with_validations(json_schema, schema, self.ValidationsMapping.bytes)70        return json_schema717273# TODO: remove when dropping support for Pydantic < v2.12.374_Attrs = {75    "default": ...,76    "default_factory": None,77    "alias": None,78    "alias_priority": None,79    "validation_alias": None,80    "serialization_alias": None,81    "title": None,82    "field_title_generator": None,83    "description": None,84    "examples": None,85    "exclude": None,86    "exclude_if": None,87    "discriminator": None,88    "deprecated": None,89    "json_schema_extra": None,90    "frozen": None,91    "validate_default": None,92    "repr": True,93    "init": None,94    "init_var": None,95    "kw_only": None,96}979899# TODO: remove when dropping support for Pydantic < v2.12.3100def asdict(field_info: FieldInfo) -> dict[str, Any]:101    attributes = {}102    for attr in _Attrs:103        value = getattr(field_info, attr, Undefined)104        if value is not Undefined:105            attributes[attr] = value106    return {107        "annotation": field_info.annotation,108        "metadata": field_info.metadata,109        "attributes": attributes,110    }111112113@dataclass114class ModelField:115    field_info: FieldInfo116    name: str117    mode: Literal["validation", "serialization"] = "validation"118    config: ConfigDict | None = None119120    @property121    def alias(self) -> str:122        a = self.field_info.alias123        return a if a is not None else self.name124125    @property126    def validation_alias(self) -> str | None:127        va = self.field_info.validation_alias128        if isinstance(va, str) and va:129            return va130        return None131132    @property133    def serialization_alias(self) -> str | None:134        sa = self.field_info.serialization_alias135        return sa or None136137    @property138    def default(self) -> Any:139        return self.get_default()140141    def __post_init__(self) -> None:142        with warnings.catch_warnings():143            # Pydantic >= 2.12.0 warns about field specific metadata that is unused144            # (e.g. `TypeAdapter(Annotated[int, Field(alias='b')])`). In some cases, we145            # end up building the type adapter from a model field annotation so we146            # need to ignore the warning:147            if shared.PYDANTIC_VERSION_MINOR_TUPLE >= (2, 12):148                from pydantic.warnings import UnsupportedFieldAttributeWarning149150                warnings.simplefilter(151                    "ignore", category=UnsupportedFieldAttributeWarning152                )153            # TODO: remove after setting the min Pydantic to v2.12.3154            # that adds asdict(), and use self.field_info.asdict() instead155            field_dict = asdict(self.field_info)156            annotated_args = (157                field_dict["annotation"],158                *field_dict["metadata"],159                # this FieldInfo needs to be created again so that it doesn't include160                # the old field info metadata and only the rest of the attributes161                Field(**field_dict["attributes"]),162            )163            self._type_adapter: TypeAdapter[Any] = TypeAdapter(164                Annotated[annotated_args],  # ty: ignore[invalid-type-form]165                config=self.config,166            )167168    def get_default(self) -> Any:169        if self.field_info.is_required():170            return Undefined171        return self.field_info.get_default(call_default_factory=True)172173    def validate(174        self,175        value: Any,176        values: dict[str, Any] = {},  # noqa: B006177        *,178        loc: tuple[int | str, ...] = (),179    ) -> tuple[Any, list[dict[str, Any]]]:180        try:181            return (182                self._type_adapter.validate_python(value, from_attributes=True),183                [],184            )185        except ValidationError as exc:186            return None, _regenerate_error_with_loc(187                errors=exc.errors(include_url=False), loc_prefix=loc188            )189190    def serialize(191        self,192        value: Any,193        *,194        mode: Literal["json", "python"] = "json",195        include: IncEx | None = None,196        exclude: IncEx | None = None,197        by_alias: bool = True,198        exclude_unset: bool = False,199        exclude_defaults: bool = False,200        exclude_none: bool = False,201    ) -> Any:202        # What calls this code passes a value that already called203        # self._type_adapter.validate_python(value)204        return self._type_adapter.dump_python(205            value,206            mode=mode,207            include=include,208            exclude=exclude,209            by_alias=by_alias,210            exclude_unset=exclude_unset,211            exclude_defaults=exclude_defaults,212            exclude_none=exclude_none,213        )214215    def serialize_json(216        self,217        value: Any,218        *,219        include: IncEx | None = None,220        exclude: IncEx | None = None,221        by_alias: bool = True,222        exclude_unset: bool = False,223        exclude_defaults: bool = False,224        exclude_none: bool = False,225    ) -> bytes:226        # What calls this code passes a value that already called227        # self._type_adapter.validate_python(value)228        # This uses Pydantic's dump_json() which serializes directly to JSON229        # bytes in one pass (via Rust), avoiding the intermediate Python dict230        # step of dump_python(mode="json") + json.dumps().231        return self._type_adapter.dump_json(232            value,233            include=include,234            exclude=exclude,235            by_alias=by_alias,236            exclude_unset=exclude_unset,237            exclude_defaults=exclude_defaults,238            exclude_none=exclude_none,239        )240241    def __hash__(self) -> int:242        # Each ModelField is unique for our purposes, to allow making a dict from243        # ModelField to its JSON Schema.244        return id(self)245246247def _has_computed_fields(field: ModelField) -> bool:248    computed_fields = field._type_adapter.core_schema.get("schema", {}).get(249        "computed_fields", []250    )251    return len(computed_fields) > 0252253254def get_schema_from_model_field(255    *,256    field: ModelField,257    model_name_map: ModelNameMap,258    field_mapping: dict[259        tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue260    ],261    separate_input_output_schemas: bool = True,262) -> dict[str, Any]:263    override_mode: Literal["validation"] | None = (264        None265        if (separate_input_output_schemas or _has_computed_fields(field))266        else "validation"267    )268    field_alias = (269        (field.validation_alias or field.alias)270        if field.mode == "validation"271        else (field.serialization_alias or field.alias)272    )273274    # This expects that GenerateJsonSchema was already used to generate the definitions275    json_schema = field_mapping[(field, override_mode or field.mode)]276    if "$ref" not in json_schema:277        # TODO remove when deprecating Pydantic v1278        # Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207279        json_schema["title"] = field.field_info.title or field_alias.title().replace(280            "_", " "281        )282    return json_schema283284285def get_definitions(286    *,287    fields: Sequence[ModelField],288    model_name_map: ModelNameMap,289    separate_input_output_schemas: bool = True,290) -> tuple[291    dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue],292    dict[str, dict[str, Any]],293]:294    schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)295    validation_fields = [field for field in fields if field.mode == "validation"]296    serialization_fields = [field for field in fields if field.mode == "serialization"]297    flat_validation_models = get_flat_models_from_fields(298        validation_fields, known_models=set()299    )300    flat_serialization_models = get_flat_models_from_fields(301        serialization_fields, known_models=set()302    )303    flat_validation_model_fields = [304        ModelField(305            field_info=FieldInfo(annotation=model),306            name=model.__name__,307            mode="validation",308        )309        for model in flat_validation_models310    ]311    flat_serialization_model_fields = [312        ModelField(313            field_info=FieldInfo(annotation=model),314            name=model.__name__,315            mode="serialization",316        )317        for model in flat_serialization_models318    ]319    flat_model_fields = flat_validation_model_fields + flat_serialization_model_fields320    input_types = {f.field_info.annotation for f in fields}321    unique_flat_model_fields = {322        f for f in flat_model_fields if f.field_info.annotation not in input_types323    }324    inputs = [325        (326            field,327            (328                field.mode329                if (separate_input_output_schemas or _has_computed_fields(field))330                else "validation"331            ),332            field._type_adapter.core_schema,333        )334        for field in list(fields) + list(unique_flat_model_fields)335    ]336    field_mapping, definitions = schema_generator.generate_definitions(inputs=inputs)337    for item_def in cast(dict[str, dict[str, Any]], definitions).values():338        if "description" in item_def:339            item_description = cast(str, item_def["description"]).split("\f")[0]340            item_def["description"] = item_description341    # definitions: dict[DefsRef, dict[str, Any]]342    # but mypy complains about general str in other places that are not declared as343    # DefsRef, although DefsRef is just str:344    # DefsRef = NewType('DefsRef', str)345    # So, a cast to simplify the types here346    return field_mapping, cast(dict[str, dict[str, Any]], definitions)347348349def is_scalar_field(field: ModelField) -> bool:350    from fastapi import params351352    return shared.field_annotation_is_scalar(353        field.field_info.annotation354    ) and not isinstance(field.field_info, params.Body)355356357def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:358    cls = type(field_info)359    merged_field_info = cls.from_annotation(annotation)360    new_field_info = copy(field_info)361    new_field_info.metadata = merged_field_info.metadata362    new_field_info.annotation = merged_field_info.annotation363    return new_field_info364365366def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:367    origin_type = get_origin(field.field_info.annotation) or field.field_info.annotation368    if origin_type is Union or origin_type is UnionType:  # Handle optional sequences369        union_args = get_args(field.field_info.annotation)370        for union_arg in union_args:371            if union_arg is type(None):372                continue373            origin_type = get_origin(union_arg) or union_arg374            break375    assert issubclass(origin_type, shared.sequence_types)  # type: ignore[arg-type]  # ty: ignore[invalid-argument-type]376    return shared.sequence_annotation_to_type[origin_type](value)  # type: ignore[no-any-return,index]  # ty: ignore[invalid-return-type]377378379def get_missing_field_error(loc: tuple[int | str, ...]) -> dict[str, Any]:380    error = ValidationError.from_exception_data(381        "Field required", [{"type": "missing", "loc": loc, "input": {}}]382    ).errors(include_url=False)[0]383    error["input"] = None384    return error  # type: ignore[return-value]  # ty: ignore[invalid-return-type]385386387def create_body_model(388    *, fields: Sequence[ModelField], model_name: str389) -> type[BaseModel]:390    field_params = {f.name: (f.field_info.annotation, f.field_info) for f in fields}391    BodyModel: type[BaseModel] = create_model(model_name, **field_params)  # type: ignore[call-overload]  # ty: ignore[no-matching-overload]392    return BodyModel393394395def get_model_fields(model: type[BaseModel]) -> list[ModelField]:396    model_fields: list[ModelField] = []397    for name, field_info in model.model_fields.items():398        type_ = field_info.annotation399        if lenient_issubclass(type_, (BaseModel, dict)) or is_dataclass(type_):400            model_config = None401        else:402            model_config = model.model_config403        model_fields.append(404            ModelField(405                field_info=field_info,406                name=name,407                config=model_config,408            )409        )410    return model_fields411412413@lru_cache414def get_cached_model_fields(model: type[BaseModel]) -> list[ModelField]:415    return get_model_fields(model)416417418# Duplicate of several schema functions from Pydantic v1 to make them compatible with419# Pydantic v2 and allow mixing the models420421TypeModelOrEnum = type["BaseModel"] | type[Enum]422TypeModelSet = set[TypeModelOrEnum]423424425def normalize_name(name: str) -> str:426    return re.sub(r"[^a-zA-Z0-9.\-_]", "_", name)427428429def get_model_name_map(unique_models: TypeModelSet) -> dict[TypeModelOrEnum, str]:430    name_model_map = {}431    for model in unique_models:432        model_name = normalize_name(model.__name__)433        name_model_map[model_name] = model434    return {v: k for k, v in name_model_map.items()}435436437def get_flat_models_from_model(438    model: type["BaseModel"], known_models: TypeModelSet | None = None439) -> TypeModelSet:440    known_models = known_models or set()441    fields = get_model_fields(model)442    get_flat_models_from_fields(fields, known_models=known_models)443    return known_models444445446def get_flat_models_from_annotation(447    annotation: Any, known_models: TypeModelSet448) -> TypeModelSet:449    origin = get_origin(annotation)450    if origin is not None:451        for arg in get_args(annotation):452            if lenient_issubclass(arg, (BaseModel, Enum)):453                if arg not in known_models:454                    known_models.add(arg)  # type: ignore[arg-type]455                    if lenient_issubclass(arg, BaseModel):456                        get_flat_models_from_model(arg, known_models=known_models)457            else:458                get_flat_models_from_annotation(arg, known_models=known_models)459    return known_models460461462def get_flat_models_from_field(463    field: ModelField, known_models: TypeModelSet464) -> TypeModelSet:465    field_type = field.field_info.annotation466    if lenient_issubclass(field_type, BaseModel):467        if field_type in known_models:468            return known_models469        known_models.add(field_type)470        get_flat_models_from_model(field_type, known_models=known_models)471    elif lenient_issubclass(field_type, Enum):472        known_models.add(field_type)473    else:474        get_flat_models_from_annotation(field_type, known_models=known_models)475    return known_models476477478def get_flat_models_from_fields(479    fields: Sequence[ModelField], known_models: TypeModelSet480) -> TypeModelSet:481    for field in fields:482        get_flat_models_from_field(field, known_models=known_models)483    return known_models484485486def _regenerate_error_with_loc(487    *, errors: Sequence[Any], loc_prefix: tuple[str | int, ...]488) -> list[dict[str, Any]]:489    updated_loc_errors: list[Any] = [490        {**err, "loc": loc_prefix + err.get("loc", ())} for err in errors491    ]492493    return updated_loc_errors

Code quality findings 32

Ensure functions have docstrings for documentation
missing-docstring
def evaluate_forwardref(
Ensure functions have docstrings for documentation
missing-docstring
def bytes_schema(self, schema: CoreSchema) -> JsonSchemaValue:
Ensure functions have docstrings for documentation
missing-docstring
def asdict(field_info: FieldInfo) -> dict[str, Any]:
Ensure functions have docstrings for documentation
missing-docstring
def alias(self) -> str:
Ensure functions have docstrings for documentation
missing-docstring
def validation_alias(self) -> str | None:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(va, str) and va:
Ensure functions have docstrings for documentation
missing-docstring
def serialization_alias(self) -> str | None:
Ensure functions have docstrings for documentation
missing-docstring
def default(self) -> Any:
Ensure functions have docstrings for documentation
missing-docstring
def get_default(self) -> Any:
Ensure functions have docstrings for documentation
missing-docstring
def validate(
Ensure functions have docstrings for documentation
missing-docstring
def serialize(
Ensure functions have docstrings for documentation
missing-docstring
def serialize_json(
Ensure functions have docstrings for documentation
missing-docstring
def get_schema_from_model_field(
Ensure functions have docstrings for documentation
missing-docstring
def get_definitions(
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
for field in list(fields) + list(unique_flat_model_fields)
Ensure functions have docstrings for documentation
missing-docstring
for item_def in cast(dict[str, dict[str, Any]], definitions).values():
Ensure functions have docstrings for documentation
missing-docstring
def is_scalar_field(field: ModelField) -> bool:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
) and not isinstance(field.field_info, params.Body)
Ensure functions have docstrings for documentation
missing-docstring
def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
Use isinstance() for type checking instead of type()
type-check
cls = type(field_info)
Ensure functions have docstrings for documentation
missing-docstring
def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
Use isinstance() for type checking instead of type()
type-check
if union_arg is type(None):
Ensure functions have docstrings for documentation
missing-docstring
def get_missing_field_error(loc: tuple[int | str, ...]) -> dict[str, Any]:
Ensure functions have docstrings for documentation
missing-docstring
def create_body_model(
Ensure functions have docstrings for documentation
missing-docstring
def get_model_fields(model: type[BaseModel]) -> list[ModelField]:
Ensure functions have docstrings for documentation
missing-docstring
def get_cached_model_fields(model: type[BaseModel]) -> list[ModelField]:
Ensure functions have docstrings for documentation
missing-docstring
def normalize_name(name: str) -> str:
Ensure functions have docstrings for documentation
missing-docstring
def get_model_name_map(unique_models: TypeModelSet) -> dict[TypeModelOrEnum, str]:
Ensure functions have docstrings for documentation
missing-docstring
def get_flat_models_from_model(
Ensure functions have docstrings for documentation
missing-docstring
def get_flat_models_from_annotation(
Ensure functions have docstrings for documentation
missing-docstring
def get_flat_models_from_field(
Ensure functions have docstrings for documentation
missing-docstring
def get_flat_models_from_fields(

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.