mirror of https://github.com/tiangolo/fastapi.git
Merge branch 'master' into fix-issue-13399-form-fields-set
This commit is contained in:
commit
a320bc6c27
|
|
@ -7,6 +7,11 @@ hide:
|
|||
|
||||
## Latest Changes
|
||||
|
||||
### Refactors
|
||||
|
||||
* ♻️ Simplify internals, remove Pydantic v1 only logic, no longer needed. PR [#14857](https://github.com/fastapi/fastapi/pull/14857) by [@tiangolo](https://github.com/tiangolo).
|
||||
* ♻️ Refactor internals, cleanup unneeded Pydantic v1 specific logic. PR [#14856](https://github.com/fastapi/fastapi/pull/14856) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Translations
|
||||
|
||||
* 🌐 Update translations for fr (outdated pages). PR [#14839](https://github.com/fastapi/fastapi/pull/14839) by [@YuriiMotov](https://github.com/YuriiMotov).
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
from .shared import PYDANTIC_V2 as PYDANTIC_V2
|
||||
from .shared import PYDANTIC_VERSION_MINOR_TUPLE as PYDANTIC_VERSION_MINOR_TUPLE
|
||||
from .shared import annotation_is_pydantic_v1 as annotation_is_pydantic_v1
|
||||
from .shared import field_annotation_is_scalar as field_annotation_is_scalar
|
||||
|
|
@ -26,7 +25,6 @@ from .v2 import copy_field_info as copy_field_info
|
|||
from .v2 import create_body_model as create_body_model
|
||||
from .v2 import evaluate_forwardref as evaluate_forwardref
|
||||
from .v2 import get_cached_model_fields as get_cached_model_fields
|
||||
from .v2 import get_compat_model_name_map as get_compat_model_name_map
|
||||
from .v2 import get_definitions as get_definitions
|
||||
from .v2 import get_missing_field_error as get_missing_field_error
|
||||
from .v2 import get_schema_from_model_field as get_schema_from_model_field
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ else:
|
|||
) # 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 = {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import re
|
||||
import warnings
|
||||
from collections.abc import Sequence
|
||||
from copy import copy, deepcopy
|
||||
from copy import copy
|
||||
from dataclasses import dataclass, is_dataclass
|
||||
from enum import Enum
|
||||
from functools import lru_cache
|
||||
|
|
@ -169,11 +169,11 @@ class ModelField:
|
|||
values: dict[str, Any] = {}, # noqa: B006
|
||||
*,
|
||||
loc: tuple[Union[int, str], ...] = (),
|
||||
) -> tuple[Any, Union[list[dict[str, Any]], None]]:
|
||||
) -> tuple[Any, list[dict[str, Any]]]:
|
||||
try:
|
||||
return (
|
||||
self._type_adapter.validate_python(value, from_attributes=True),
|
||||
None,
|
||||
[],
|
||||
)
|
||||
except ValidationError as exc:
|
||||
return None, _regenerate_error_with_loc(
|
||||
|
|
@ -305,94 +305,12 @@ def get_definitions(
|
|||
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":
|
||||
value = schema["$ref"]
|
||||
if isinstance(value, str):
|
||||
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(model=new_name)
|
||||
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
|
||||
],
|
||||
) -> tuple[
|
||||
dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue],
|
||||
dict[str, Any],
|
||||
]:
|
||||
old_name_to_new_name_map = {}
|
||||
for field_key, schema in field_mapping.items():
|
||||
model = field_key[0].type_
|
||||
if model not in model_name_map or "$ref" not in schema:
|
||||
continue
|
||||
new_name = model_name_map[model]
|
||||
old_name = schema["$ref"].split("/")[-1]
|
||||
if old_name in {f"{new_name}-Input", f"{new_name}-Output"}:
|
||||
continue
|
||||
old_name_to_new_name_map[old_name] = new_name
|
||||
|
||||
new_field_mapping: dict[
|
||||
tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
|
||||
] = {}
|
||||
for field_key, schema in field_mapping.items():
|
||||
new_schema = _replace_refs(
|
||||
schema=schema,
|
||||
old_name_to_new_name_map=old_name_to_new_name_map,
|
||||
)
|
||||
new_field_mapping[field_key] = new_schema
|
||||
|
||||
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
|
||||
# definitions: dict[DefsRef, dict[str, Any]]
|
||||
# but mypy complains about general str in other places that are not declared as
|
||||
# DefsRef, although DefsRef is just str:
|
||||
# DefsRef = NewType('DefsRef', str)
|
||||
# So, a cast to simplify the types here
|
||||
return field_mapping, cast(dict[str, dict[str, Any]], definitions)
|
||||
|
||||
|
||||
def is_scalar_field(field: ModelField) -> bool:
|
||||
|
|
@ -441,7 +359,7 @@ def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
|
|||
return shared.sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return,index]
|
||||
|
||||
|
||||
def get_missing_field_error(loc: tuple[str, ...]) -> dict[str, Any]:
|
||||
def get_missing_field_error(loc: tuple[Union[int, str], ...]) -> dict[str, Any]:
|
||||
error = ValidationError.from_exception_data(
|
||||
"Field required", [{"type": "missing", "loc": loc, "input": {}}]
|
||||
).errors(include_url=False)[0]
|
||||
|
|
@ -499,17 +417,6 @@ def get_model_name_map(unique_models: TypeModelSet) -> dict[TypeModelOrEnum, str
|
|||
return {v: k for k, v in name_model_map.items()}
|
||||
|
||||
|
||||
def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap:
|
||||
all_flat_models: TypeModelSet = set()
|
||||
|
||||
v2_model_fields = [field for field in fields if isinstance(field, ModelField)]
|
||||
v2_flat_models = get_flat_models_from_fields(v2_model_fields, known_models=set())
|
||||
all_flat_models = all_flat_models.union(v2_flat_models)
|
||||
|
||||
model_name_map = get_model_name_map(all_flat_models)
|
||||
return model_name_map
|
||||
|
||||
|
||||
def get_flat_models_from_model(
|
||||
model: type["BaseModel"], known_models: Union[TypeModelSet, None] = None
|
||||
) -> TypeModelSet:
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ from fastapi._compat import (
|
|||
ModelField,
|
||||
RequiredParam,
|
||||
Undefined,
|
||||
_regenerate_error_with_loc,
|
||||
copy_field_info,
|
||||
create_body_model,
|
||||
evaluate_forwardref,
|
||||
|
|
@ -718,12 +717,7 @@ def _validate_value_with_model_field(
|
|||
return None, [get_missing_field_error(loc=loc)]
|
||||
else:
|
||||
return deepcopy(field.default), []
|
||||
v_, errors_ = field.validate(value, values, loc=loc)
|
||||
if isinstance(errors_, list):
|
||||
new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
|
||||
return None, new_errors
|
||||
else:
|
||||
return v_, []
|
||||
return field.validate(value, values, loc=loc)
|
||||
|
||||
|
||||
def _is_json_field(field: ModelField) -> bool:
|
||||
|
|
|
|||
|
|
@ -9,11 +9,14 @@ from fastapi import routing
|
|||
from fastapi._compat import (
|
||||
ModelField,
|
||||
Undefined,
|
||||
get_compat_model_name_map,
|
||||
get_definitions,
|
||||
get_schema_from_model_field,
|
||||
lenient_issubclass,
|
||||
)
|
||||
from fastapi._compat.v2 import (
|
||||
get_flat_models_from_fields,
|
||||
get_model_name_map,
|
||||
)
|
||||
from fastapi.datastructures import DefaultPlaceholder
|
||||
from fastapi.dependencies.models import Dependant
|
||||
from fastapi.dependencies.utils import (
|
||||
|
|
@ -512,7 +515,8 @@ def get_openapi(
|
|||
webhook_paths: dict[str, dict[str, Any]] = {}
|
||||
operation_ids: set[str] = set()
|
||||
all_fields = get_fields_from_routes(list(routes or []) + list(webhooks or []))
|
||||
model_name_map = get_compat_model_name_map(all_fields)
|
||||
flat_models = get_flat_models_from_fields(all_fields, known_models=set())
|
||||
model_name_map = get_model_name_map(flat_models)
|
||||
field_mapping, definitions = get_definitions(
|
||||
fields=all_fields,
|
||||
model_name_map=model_name_map,
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ from fastapi.exceptions import (
|
|||
)
|
||||
from fastapi.types import DecoratedCallable, IncEx
|
||||
from fastapi.utils import (
|
||||
create_cloned_field,
|
||||
create_model_field,
|
||||
generate_unique_id,
|
||||
get_value_or_default,
|
||||
|
|
@ -278,15 +277,12 @@ async def serialize_response(
|
|||
endpoint_ctx: Optional[EndpointContext] = None,
|
||||
) -> Any:
|
||||
if field:
|
||||
errors = []
|
||||
if is_coroutine:
|
||||
value, errors_ = field.validate(response_content, {}, loc=("response",))
|
||||
value, errors = field.validate(response_content, {}, loc=("response",))
|
||||
else:
|
||||
value, errors_ = await run_in_threadpool(
|
||||
value, errors = await run_in_threadpool(
|
||||
field.validate, response_content, {}, loc=("response",)
|
||||
)
|
||||
if isinstance(errors_, list):
|
||||
errors.extend(errors_)
|
||||
if errors:
|
||||
ctx = endpoint_ctx or EndpointContext()
|
||||
raise ResponseValidationError(
|
||||
|
|
@ -652,20 +648,8 @@ class APIRoute(routing.Route):
|
|||
type_=self.response_model,
|
||||
mode="serialization",
|
||||
)
|
||||
# Create a clone of the field, so that a Pydantic submodel is not returned
|
||||
# as is just because it's an instance of a subclass of a more limited class
|
||||
# e.g. UserInDB (containing hashed_password) could be a subclass of User
|
||||
# that doesn't have the hashed_password. But because it's a subclass, it
|
||||
# would pass the validation and be returned as is.
|
||||
# By being a new field, no inheritance will be passed as is. A new model
|
||||
# will always be created.
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
self.secure_cloned_response_field: Optional[ModelField] = (
|
||||
create_cloned_field(self.response_field)
|
||||
)
|
||||
else:
|
||||
self.response_field = None # type: ignore
|
||||
self.secure_cloned_response_field = None
|
||||
self.dependencies = list(dependencies or [])
|
||||
self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "")
|
||||
# if a "form feed" character (page break) is found in the description text,
|
||||
|
|
@ -720,7 +704,7 @@ class APIRoute(routing.Route):
|
|||
body_field=self.body_field,
|
||||
status_code=self.status_code,
|
||||
response_class=self.response_class,
|
||||
response_field=self.secure_cloned_response_field,
|
||||
response_field=self.response_field,
|
||||
response_model_include=self.response_model_include,
|
||||
response_model_exclude=self.response_model_exclude,
|
||||
response_model_by_alias=self.response_model_by_alias,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import re
|
||||
import warnings
|
||||
from collections.abc import MutableMapping
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Optional,
|
||||
Union,
|
||||
)
|
||||
from weakref import WeakKeyDictionary
|
||||
|
||||
import fastapi
|
||||
from fastapi._compat import (
|
||||
|
|
@ -21,7 +19,6 @@ from fastapi._compat import (
|
|||
)
|
||||
from fastapi.datastructures import DefaultPlaceholder, DefaultType
|
||||
from fastapi.exceptions import FastAPIDeprecationWarning, PydanticV1NotSupportedError
|
||||
from pydantic import BaseModel
|
||||
from pydantic.fields import FieldInfo
|
||||
from typing_extensions import Literal
|
||||
|
||||
|
|
@ -30,11 +27,6 @@ from ._compat import v2
|
|||
if TYPE_CHECKING: # pragma: nocover
|
||||
from .routing import APIRoute
|
||||
|
||||
# Cache for `create_cloned_field`
|
||||
_CLONED_TYPES_CACHE: MutableMapping[type[BaseModel], type[BaseModel]] = (
|
||||
WeakKeyDictionary()
|
||||
)
|
||||
|
||||
|
||||
def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool:
|
||||
if status_code is None:
|
||||
|
|
@ -97,14 +89,6 @@ def create_model_field(
|
|||
) from None
|
||||
|
||||
|
||||
def create_cloned_field(
|
||||
field: ModelField,
|
||||
*,
|
||||
cloned_types: Optional[MutableMapping[type[BaseModel], type[BaseModel]]] = None,
|
||||
) -> ModelField:
|
||||
return field
|
||||
|
||||
|
||||
def generate_operation_id_for_path(
|
||||
*, name: str, path: str, method: str
|
||||
) -> str: # pragma: nocover
|
||||
|
|
|
|||
Loading…
Reference in New Issue