Browse Source

♻️ Refactor internals to not use Pydantic v1

pull/14609/head
Sebastián Ramírez 7 months ago
parent
commit
aa11eb51a2
  1. 15
      fastapi/datastructures.py
  2. 84
      fastapi/dependencies/utils.py
  3. 25
      fastapi/encoders.py
  4. 22
      fastapi/openapi/models.py
  5. 7
      fastapi/openapi/utils.py
  6. 50
      fastapi/routing.py
  7. 109
      fastapi/utils.py

15
fastapi/datastructures.py

@ -1,3 +1,4 @@
from collections.abc import Mapping
from typing import (
Annotated,
Any,
@ -9,11 +10,7 @@ from typing import (
)
from annotated_doc import Doc
from fastapi._compat import (
CoreSchema,
GetJsonSchemaHandler,
JsonSchemaValue,
)
from pydantic import GetJsonSchemaHandler
from starlette.datastructures import URL as URL # noqa: F401
from starlette.datastructures import Address as Address # noqa: F401
from starlette.datastructures import FormData as FormData # noqa: F401
@ -142,14 +139,14 @@ class UploadFile(StarletteUploadFile):
@classmethod
def __get_pydantic_json_schema__(
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
) -> JsonSchemaValue:
cls, core_schema: Mapping[str, Any], handler: GetJsonSchemaHandler
) -> dict[str, Any]:
return {"type": "string", "format": "binary"}
@classmethod
def __get_pydantic_core_schema__(
cls, source: type[Any], handler: Callable[[Any], CoreSchema]
) -> CoreSchema:
cls, source: type[Any], handler: Callable[[Any], Mapping[str, Any]]
) -> Mapping[str, Any]:
from ._compat.v2 import with_info_plain_validator_function
return with_info_plain_validator_function(cls._validate)

84
fastapi/dependencies/utils.py

@ -21,7 +21,6 @@ from fastapi._compat import (
ModelField,
RequiredParam,
Undefined,
_is_error_wrapper,
_is_model_class,
copy_field_info,
create_body_model,
@ -43,7 +42,6 @@ from fastapi._compat import (
serialize_sequence_value,
value_is_sequence,
)
from fastapi._compat.shared import annotation_is_pydantic_v1
from fastapi.background import BackgroundTasks
from fastapi.concurrency import (
asynccontextmanager,
@ -71,8 +69,6 @@ from starlette.responses import Response
from starlette.websockets import WebSocket
from typing_extensions import Literal, get_args, get_origin
from .. import temp_pydantic_v1_params
multipart_not_installed_error = (
'Form data requires "python-multipart" to be installed. \n'
'You can install "python-multipart" with: \n\n'
@ -327,9 +323,7 @@ def get_dependant(
"pydantic.v1 models are no longer supported by FastAPI."
f" Please update the param {param_name}: {param_details.type_annotation!r}."
)
if isinstance(
param_details.field.field_info, (params.Body, temp_pydantic_v1_params.Body)
):
if isinstance(param_details.field.field_info, params.Body):
dependant.body_params.append(param_details.field)
else:
add_param_to_fields(field=param_details.field, dependant=dependant)
@ -388,7 +382,7 @@ def analyze_param(
fastapi_annotations = [
arg
for arg in annotated_args[1:]
if isinstance(arg, (FieldInfo, may_v1.FieldInfo, params.Depends))
if isinstance(arg, (FieldInfo, params.Depends))
]
fastapi_specific_annotations = [
arg
@ -397,30 +391,27 @@ def analyze_param(
arg,
(
params.Param,
temp_pydantic_v1_params.Param,
params.Body,
temp_pydantic_v1_params.Body,
params.Depends,
),
)
]
if fastapi_specific_annotations:
fastapi_annotation: Union[
FieldInfo, may_v1.FieldInfo, params.Depends, None
] = fastapi_specific_annotations[-1]
fastapi_annotation: Union[FieldInfo, params.Depends, None] = (
fastapi_specific_annotations[-1]
)
else:
fastapi_annotation = None
# Set default for Annotated FieldInfo
if isinstance(fastapi_annotation, (FieldInfo, may_v1.FieldInfo)):
if isinstance(fastapi_annotation, FieldInfo):
# Copy `field_info` because we mutate `field_info.default` below.
field_info = copy_field_info(
field_info=fastapi_annotation, # type: ignore[arg-type]
annotation=use_annotation,
)
assert field_info.default in {
Undefined,
may_v1.Undefined,
} or field_info.default in {RequiredParam, may_v1.RequiredParam}, (
assert (
field_info.default == Undefined or field_info.default == RequiredParam
), (
f"`{field_info.__class__.__name__}` default value cannot be set in"
f" `Annotated` for {param_name!r}. Set the default value with `=` instead."
)
@ -444,7 +435,7 @@ def analyze_param(
)
depends = value
# Get FieldInfo from default value
elif isinstance(value, (FieldInfo, may_v1.FieldInfo)):
elif isinstance(value, FieldInfo):
assert field_info is None, (
"Cannot specify FastAPI annotations in `Annotated` and default value"
f" together for {param_name!r}"
@ -488,14 +479,7 @@ def analyze_param(
) or is_uploadfile_sequence_annotation(type_annotation):
field_info = params.File(annotation=use_annotation, default=default_value)
elif not field_annotation_is_scalar(annotation=type_annotation):
if annotation_is_pydantic_v1(use_annotation):
field_info = temp_pydantic_v1_params.Body( # type: ignore[assignment]
annotation=use_annotation, default=default_value
)
else:
field_info = params.Body(
annotation=use_annotation, default=default_value
)
field_info = params.Body(annotation=use_annotation, default=default_value)
else:
field_info = params.Query(annotation=use_annotation, default=default_value)
@ -504,14 +488,12 @@ def analyze_param(
if field_info is not None:
# Handle field_info.in_
if is_path_param:
assert isinstance(
field_info, (params.Path, temp_pydantic_v1_params.Path)
), (
assert isinstance(field_info, params.Path), (
f"Cannot use `{field_info.__class__.__name__}` for path param"
f" {param_name!r}"
)
elif (
isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param))
isinstance(field_info, params.Param)
and getattr(field_info, "in_", None) is None
):
field_info.in_ = params.ParamTypes.query
@ -520,7 +502,7 @@ def analyze_param(
field_info,
param_name,
)
if isinstance(field_info, (params.Form, temp_pydantic_v1_params.Form)):
if isinstance(field_info, params.Form):
ensure_multipart_is_installed()
if not field_info.alias and getattr(field_info, "convert_underscores", None):
alias = param_name.replace("_", "-")
@ -532,15 +514,14 @@ def analyze_param(
type_=use_annotation_from_field_info,
default=field_info.default,
alias=alias,
required=field_info.default
in (RequiredParam, may_v1.RequiredParam, Undefined),
required=field_info.default in (RequiredParam, Undefined),
field_info=field_info,
)
if is_path_param:
assert is_scalar_field(field=field), (
"Path params must be of one of the supported types"
)
elif isinstance(field_info, (params.Query, temp_pydantic_v1_params.Query)):
elif isinstance(field_info, params.Query):
assert (
is_scalar_field(field)
or is_scalar_sequence_field(field)
@ -739,9 +720,7 @@ def _validate_value_with_model_field(
else:
return deepcopy(field.default), []
v_, errors_ = field.validate(value, values, loc=loc)
if _is_error_wrapper(errors_): # type: ignore[arg-type]
return None, [errors_]
elif isinstance(errors_, list):
if isinstance(errors_, list):
new_errors = may_v1._regenerate_error_with_loc(errors=errors_, loc_prefix=())
return None, new_errors
else:
@ -759,7 +738,7 @@ def _get_multidict_value(
if (
value is None
or (
isinstance(field.field_info, (params.Form, temp_pydantic_v1_params.Form))
isinstance(field.field_info, params.Form)
and isinstance(value, str) # For type checks
and value == ""
)
@ -829,7 +808,7 @@ def request_params_to_args(
if single_not_embedded_field:
field_info = first_field.field_info
assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), (
assert isinstance(field_info, params.Param), (
"Params must be subclasses of Param"
)
loc: tuple[str, ...] = (field_info.in_.value,)
@ -841,7 +820,7 @@ def request_params_to_args(
for field in fields:
value = _get_multidict_value(field, received_params)
field_info = field.field_info
assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), (
assert isinstance(field_info, params.Param), (
"Params must be subclasses of Param"
)
loc = (field_info.in_.value, get_validation_alias(field))
@ -890,7 +869,7 @@ def _should_embed_body_fields(fields: list[ModelField]) -> bool:
# If it's a Form (or File) field, it has to be a BaseModel (or a union of BaseModels) to be top level
# otherwise it has to be embedded, so that the key value pair can be extracted
if (
isinstance(first_field.field_info, (params.Form, temp_pydantic_v1_params.Form))
isinstance(first_field.field_info, params.Form)
and not _is_model_class(first_field.type_)
and not is_union_of_base_models(first_field.type_)
):
@ -908,14 +887,14 @@ async def _extract_form_body(
value = _get_multidict_value(field, received_body)
field_info = field.field_info
if (
isinstance(field_info, (params.File, temp_pydantic_v1_params.File))
isinstance(field_info, params.File)
and is_bytes_field(field)
and isinstance(value, UploadFile)
):
value = await value.read()
elif (
is_bytes_sequence_field(field)
and isinstance(field_info, (params.File, temp_pydantic_v1_params.File))
and isinstance(field_info, params.File)
and value_is_sequence(value)
):
# For types
@ -1026,28 +1005,15 @@ def get_body_field(
BodyFieldInfo_kwargs["default"] = None
if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params):
BodyFieldInfo: type[params.Body] = params.File
elif any(
isinstance(f.field_info, temp_pydantic_v1_params.File)
for f in flat_dependant.body_params
):
BodyFieldInfo: type[temp_pydantic_v1_params.Body] = temp_pydantic_v1_params.File # type: ignore[no-redef]
elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params):
BodyFieldInfo = params.Form
elif any(
isinstance(f.field_info, temp_pydantic_v1_params.Form)
for f in flat_dependant.body_params
):
BodyFieldInfo = temp_pydantic_v1_params.Form # type: ignore[assignment]
else:
if annotation_is_pydantic_v1(BodyModel):
BodyFieldInfo = temp_pydantic_v1_params.Body # type: ignore[assignment]
else:
BodyFieldInfo = params.Body
BodyFieldInfo = params.Body
body_param_media_types = [
f.field_info.media_type
for f in flat_dependant.body_params
if isinstance(f.field_info, (params.Body, temp_pydantic_v1_params.Body))
if isinstance(f.field_info, params.Body)
]
if len(set(body_param_media_types)) == 1:
BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0]

25
fastapi/encoders.py

@ -18,14 +18,14 @@ from typing import Annotated, Any, Callable, Optional, Union
from uuid import UUID
from annotated_doc import Doc
from fastapi._compat import may_v1
from fastapi.exceptions import PydanticV1NotSupportedError
from fastapi.types import IncEx
from pydantic import BaseModel
from pydantic.color import Color
from pydantic.networks import AnyUrl, NameEmail
from pydantic.types import SecretBytes, SecretStr
from ._compat import Url, _is_undefined, _model_dump
from ._compat import Url, _is_undefined, _model_dump, may_v1
# Taken from Pydantic v1 as is
@ -63,7 +63,6 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
ENCODERS_BY_TYPE: dict[type[Any], Callable[[Any], Any]] = {
bytes: lambda o: o.decode(),
Color: str,
may_v1.Color: str,
datetime.date: isoformat,
datetime.datetime: isoformat,
datetime.time: isoformat,
@ -80,19 +79,14 @@ ENCODERS_BY_TYPE: dict[type[Any], Callable[[Any], Any]] = {
IPv6Interface: str,
IPv6Network: str,
NameEmail: str,
may_v1.NameEmail: str,
Path: str,
Pattern: lambda o: o.pattern,
SecretBytes: str,
may_v1.SecretBytes: str,
SecretStr: str,
may_v1.SecretStr: str,
set: list,
UUID: str,
Url: str,
may_v1.Url: str,
AnyUrl: str,
may_v1.AnyUrl: str,
}
@ -224,13 +218,12 @@ def jsonable_encoder(
include = set(include)
if exclude is not None and not isinstance(exclude, (set, dict)):
exclude = set(exclude)
if isinstance(obj, (BaseModel, may_v1.BaseModel)):
# TODO: remove when deprecating Pydantic v1
encoders: dict[Any, Any] = {}
if isinstance(obj, may_v1.BaseModel):
encoders = getattr(obj.__config__, "json_encoders", {})
if custom_encoder:
encoders = {**encoders, **custom_encoder}
if isinstance(obj, may_v1.BaseModel):
raise PydanticV1NotSupportedError(
"pydantic.v1 models are no longer supported by FastAPI."
f" Please update the model {obj!r}."
)
if isinstance(obj, BaseModel):
obj_dict = _model_dump(
obj, # type: ignore[arg-type]
mode="json",
@ -247,8 +240,6 @@ def jsonable_encoder(
obj_dict,
exclude_none=exclude_none,
exclude_defaults=exclude_defaults,
# TODO: remove when deprecating Pydantic v1
custom_encoder=encoders,
sqlalchemy_safe=sqlalchemy_safe,
)
if dataclasses.is_dataclass(obj):

22
fastapi/openapi/models.py

@ -1,15 +1,15 @@
from collections.abc import Iterable
from collections.abc import Iterable, Mapping
from enum import Enum
from typing import Annotated, Any, Callable, Optional, Union
from fastapi._compat import (
CoreSchema,
from fastapi._compat import with_info_plain_validator_function
from fastapi.logger import logger
from pydantic import (
AnyUrl,
BaseModel,
Field,
GetJsonSchemaHandler,
JsonSchemaValue,
with_info_plain_validator_function,
)
from fastapi.logger import logger
from pydantic import AnyUrl, BaseModel, Field
from typing_extensions import Literal, TypedDict
from typing_extensions import deprecated as typing_deprecated
@ -43,14 +43,14 @@ except ImportError: # pragma: no cover
@classmethod
def __get_pydantic_json_schema__(
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
) -> JsonSchemaValue:
cls, core_schema: Mapping[str, Any], handler: GetJsonSchemaHandler
) -> dict[str, Any]:
return {"type": "string", "format": "email"}
@classmethod
def __get_pydantic_core_schema__(
cls, source: type[Any], handler: Callable[[Any], CoreSchema]
) -> CoreSchema:
cls, source: type[Any], handler: Callable[[Any], Mapping[str, Any]]
) -> Mapping[str, Any]:
return with_info_plain_validator_function(cls._validate)

7
fastapi/openapi/utils.py

@ -6,7 +6,6 @@ from typing import Any, Optional, Union, cast
from fastapi import routing
from fastapi._compat import (
JsonSchemaValue,
ModelField,
Undefined,
get_compat_model_name_map,
@ -109,7 +108,7 @@ def _get_openapi_operation_parameters(
dependant: Dependant,
model_name_map: ModelNameMap,
field_mapping: dict[
tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any]
],
separate_input_output_schemas: bool = True,
) -> list[dict[str, Any]]:
@ -182,7 +181,7 @@ def get_openapi_operation_request_body(
body_field: Optional[ModelField],
model_name_map: ModelNameMap,
field_mapping: dict[
tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any]
],
separate_input_output_schemas: bool = True,
) -> Optional[dict[str, Any]]:
@ -265,7 +264,7 @@ def get_openapi_path(
operation_ids: set[str],
model_name_map: ModelNameMap,
field_mapping: dict[
tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any]
],
separate_input_output_schemas: bool = True,
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:

50
fastapi/routing.py

@ -21,16 +21,13 @@ from typing import (
)
from annotated_doc import Doc
from fastapi import params, temp_pydantic_v1_params
from fastapi import params
from fastapi._compat import (
ModelField,
Undefined,
_get_model_config,
_model_dump,
_normalize_errors,
annotation_is_pydantic_v1,
lenient_issubclass,
may_v1,
)
from fastapi.datastructures import Default, DefaultPlaceholder
from fastapi.dependencies.models import Dependant
@ -154,22 +151,7 @@ def _prepare_response_content(
exclude_defaults: bool = False,
exclude_none: bool = False,
) -> Any:
if isinstance(res, may_v1.BaseModel):
read_with_orm_mode = getattr(_get_model_config(res), "read_with_orm_mode", None) # type: ignore[arg-type]
if read_with_orm_mode:
# Let from_orm extract the data from this model instead of converting
# it now to a dict.
# Otherwise, there's no way to extract lazy data that requires attribute
# access instead of dict iteration, e.g. lazy relationships.
return res
return _model_dump(
res, # type: ignore[arg-type]
by_alias=True,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
elif isinstance(res, list):
if isinstance(res, list):
return [
_prepare_response_content(
item,
@ -251,14 +233,6 @@ async def serialize_response(
) -> Any:
if field:
errors = []
if not hasattr(field, "serialize"):
# pydantic v1
response_content = _prepare_response_content(
response_content,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
if is_coroutine:
value, errors_ = field.validate(response_content, {}, loc=("response",))
else:
@ -267,8 +241,6 @@ async def serialize_response(
)
if isinstance(errors_, list):
errors.extend(errors_)
elif errors_:
errors.append(errors_)
if errors:
ctx = endpoint_ctx or EndpointContext()
raise ResponseValidationError(
@ -277,18 +249,7 @@ async def serialize_response(
endpoint_ctx=ctx,
)
if hasattr(field, "serialize"):
return field.serialize(
value,
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
return jsonable_encoder(
return field.serialize(
value,
include=include,
exclude=exclude,
@ -297,6 +258,7 @@ async def serialize_response(
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
else:
return jsonable_encoder(response_content)
@ -331,9 +293,7 @@ def get_request_handler(
) -> Callable[[Request], Coroutine[Any, Any, Response]]:
assert dependant.call is not None, "dependant.call must be a function"
is_coroutine = dependant.is_coroutine_callable
is_body_form = body_field and isinstance(
body_field.field_info, (params.Form, temp_pydantic_v1_params.Form)
)
is_body_form = body_field and isinstance(body_field.field_info, params.Form)
if isinstance(response_class, DefaultPlaceholder):
actual_response_class: type[Response] = response_class.value
else:

109
fastapi/utils.py

@ -6,7 +6,6 @@ from typing import (
Any,
Optional,
Union,
cast,
)
from weakref import WeakKeyDictionary
@ -19,11 +18,9 @@ from fastapi._compat import (
UndefinedType,
Validator,
annotation_is_pydantic_v1,
lenient_issubclass,
may_v1,
)
from fastapi.datastructures import DefaultPlaceholder, DefaultType
from fastapi.exceptions import FastAPIDeprecationWarning
from fastapi.exceptions import FastAPIDeprecationWarning, PydanticV1NotSupportedError
from pydantic import BaseModel
from pydantic.fields import FieldInfo
from typing_extensions import Literal
@ -83,52 +80,18 @@ def create_model_field(
mode: Literal["validation", "serialization"] = "validation",
version: Literal["1", "auto"] = "auto",
) -> ModelField:
class_validators = class_validators or {}
v1_model_config = may_v1.BaseConfig
v1_field_info = field_info or may_v1.FieldInfo()
v1_kwargs = {
"name": name,
"field_info": v1_field_info,
"type_": type_,
"class_validators": class_validators,
"default": default,
"required": required,
"model_config": v1_model_config,
"alias": alias,
}
if (
annotation_is_pydantic_v1(type_)
or isinstance(field_info, may_v1.FieldInfo)
or version == "1"
):
from fastapi._compat import v1
try:
return v1.ModelField(**v1_kwargs) # type: ignore[return-value]
except RuntimeError:
raise fastapi.exceptions.FastAPIError(
_invalid_args_message.format(type_=type_)
) from None
else:
field_info = field_info or FieldInfo(
annotation=type_, default=default, alias=alias
if annotation_is_pydantic_v1(type_):
raise PydanticV1NotSupportedError(
"pydantic.v1 models are no longer supported by FastAPI."
f" Please update the response model {type_!r}."
)
kwargs = {"mode": mode, "name": name, "field_info": field_info}
try:
return v2.ModelField(**kwargs) # type: ignore[return-value,arg-type]
except PydanticSchemaGenerationError:
raise fastapi.exceptions.FastAPIError(
_invalid_args_message.format(type_=type_)
) from None
# Pydantic v2 is not installed, but it's not a Pydantic v1 ModelField, it could be
# a Pydantic v1 type, like a constrained int
from fastapi._compat import v1
class_validators = class_validators or {}
field_info = field_info or FieldInfo(annotation=type_, default=default, alias=alias)
kwargs = {"mode": mode, "name": name, "field_info": field_info}
try:
return v1.ModelField(**v1_kwargs)
except RuntimeError:
return v2.ModelField(**kwargs) # type: ignore[return-value,arg-type]
except PydanticSchemaGenerationError:
raise fastapi.exceptions.FastAPIError(
_invalid_args_message.format(type_=type_)
) from None
@ -139,57 +102,7 @@ def create_cloned_field(
*,
cloned_types: Optional[MutableMapping[type[BaseModel], type[BaseModel]]] = None,
) -> ModelField:
if isinstance(field, v2.ModelField):
return field
from fastapi._compat import v1
# cloned_types caches already cloned types to support recursive models and improve
# performance by avoiding unnecessary cloning
if cloned_types is None:
cloned_types = _CLONED_TYPES_CACHE
original_type = field.type_
use_type = original_type
if lenient_issubclass(original_type, v1.BaseModel):
original_type = cast(type[v1.BaseModel], original_type)
use_type = cloned_types.get(original_type)
if use_type is None:
use_type = v1.create_model(original_type.__name__, __base__=original_type)
cloned_types[original_type] = use_type
for f in original_type.__fields__.values():
use_type.__fields__[f.name] = create_cloned_field(
f,
cloned_types=cloned_types,
)
new_field = create_model_field(name=field.name, type_=use_type, version="1")
new_field.has_alias = field.has_alias # type: ignore[attr-defined]
new_field.alias = field.alias # type: ignore[misc]
new_field.class_validators = field.class_validators # type: ignore[attr-defined]
new_field.default = field.default # type: ignore[misc]
new_field.default_factory = field.default_factory # type: ignore[attr-defined]
new_field.required = field.required # type: ignore[misc]
new_field.model_config = field.model_config # type: ignore[attr-defined]
new_field.field_info = field.field_info
new_field.allow_none = field.allow_none # type: ignore[attr-defined]
new_field.validate_always = field.validate_always # type: ignore[attr-defined]
if field.sub_fields: # type: ignore[attr-defined]
new_field.sub_fields = [ # type: ignore[attr-defined]
create_cloned_field(sub_field, cloned_types=cloned_types)
for sub_field in field.sub_fields # type: ignore[attr-defined]
]
if field.key_field: # type: ignore[attr-defined]
new_field.key_field = create_cloned_field( # type: ignore[attr-defined]
field.key_field, # type: ignore[attr-defined]
cloned_types=cloned_types,
)
new_field.validators = field.validators # type: ignore[attr-defined]
new_field.pre_validators = field.pre_validators # type: ignore[attr-defined]
new_field.post_validators = field.post_validators # type: ignore[attr-defined]
new_field.parse_json = field.parse_json # type: ignore[attr-defined]
new_field.shape = field.shape # type: ignore[attr-defined]
new_field.populate_validators() # type: ignore[attr-defined]
return new_field
return field
def generate_operation_id_for_path(

Loading…
Cancel
Save