Browse Source

Merge branch 'master' into fix-issue-13399-form-fields-set

pull/14574/head
Adarsh Bennur 4 months ago
committed by GitHub
parent
commit
a320bc6c27
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      docs/en/docs/release-notes.md
  2. 2
      fastapi/_compat/__init__.py
  3. 1
      fastapi/_compat/shared.py
  4. 113
      fastapi/_compat/v2.py
  5. 8
      fastapi/dependencies/utils.py
  6. 8
      fastapi/openapi/utils.py
  7. 22
      fastapi/routing.py
  8. 16
      fastapi/utils.py

5
docs/en/docs/release-notes.md

@ -7,6 +7,11 @@ hide:
## Latest Changes ## 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 ### Translations
* 🌐 Update translations for fr (outdated pages). PR [#14839](https://github.com/fastapi/fastapi/pull/14839) by [@YuriiMotov](https://github.com/YuriiMotov). * 🌐 Update translations for fr (outdated pages). PR [#14839](https://github.com/fastapi/fastapi/pull/14839) by [@YuriiMotov](https://github.com/YuriiMotov).

2
fastapi/_compat/__init__.py

@ -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 PYDANTIC_VERSION_MINOR_TUPLE as PYDANTIC_VERSION_MINOR_TUPLE
from .shared import annotation_is_pydantic_v1 as annotation_is_pydantic_v1 from .shared import annotation_is_pydantic_v1 as annotation_is_pydantic_v1
from .shared import field_annotation_is_scalar as field_annotation_is_scalar 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 create_body_model as create_body_model
from .v2 import evaluate_forwardref as evaluate_forwardref from .v2 import evaluate_forwardref as evaluate_forwardref
from .v2 import get_cached_model_fields as get_cached_model_fields 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_definitions as get_definitions
from .v2 import get_missing_field_error as get_missing_field_error 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 from .v2 import get_schema_from_model_field as get_schema_from_model_field

1
fastapi/_compat/shared.py

@ -28,7 +28,6 @@ else:
) # pyright: ignore[reportAttributeAccessIssue] ) # pyright: ignore[reportAttributeAccessIssue]
PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2]) 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_annotation_to_type = {

113
fastapi/_compat/v2.py

@ -1,7 +1,7 @@
import re import re
import warnings import warnings
from collections.abc import Sequence from collections.abc import Sequence
from copy import copy, deepcopy from copy import copy
from dataclasses import dataclass, is_dataclass from dataclasses import dataclass, is_dataclass
from enum import Enum from enum import Enum
from functools import lru_cache from functools import lru_cache
@ -169,11 +169,11 @@ class ModelField:
values: dict[str, Any] = {}, # noqa: B006 values: dict[str, Any] = {}, # noqa: B006
*, *,
loc: tuple[Union[int, str], ...] = (), loc: tuple[Union[int, str], ...] = (),
) -> tuple[Any, Union[list[dict[str, Any]], None]]: ) -> tuple[Any, list[dict[str, Any]]]:
try: try:
return ( return (
self._type_adapter.validate_python(value, from_attributes=True), self._type_adapter.validate_python(value, from_attributes=True),
None, [],
) )
except ValidationError as exc: except ValidationError as exc:
return None, _regenerate_error_with_loc( return None, _regenerate_error_with_loc(
@ -305,94 +305,12 @@ def get_definitions(
if "description" in item_def: if "description" in item_def:
item_description = cast(str, item_def["description"]).split("\f")[0] item_description = cast(str, item_def["description"]).split("\f")[0]
item_def["description"] = item_description item_def["description"] = item_description
new_mapping, new_definitions = _remap_definitions_and_field_mappings( # definitions: dict[DefsRef, dict[str, Any]]
model_name_map=model_name_map, # but mypy complains about general str in other places that are not declared as
definitions=definitions, # type: ignore[arg-type] # DefsRef, although DefsRef is just str:
field_mapping=field_mapping, # DefsRef = NewType('DefsRef', str)
) # So, a cast to simplify the types here
return new_mapping, new_definitions return field_mapping, cast(dict[str, dict[str, Any]], 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
def is_scalar_field(field: ModelField) -> bool: 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] 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( error = ValidationError.from_exception_data(
"Field required", [{"type": "missing", "loc": loc, "input": {}}] "Field required", [{"type": "missing", "loc": loc, "input": {}}]
).errors(include_url=False)[0] ).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()} 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( def get_flat_models_from_model(
model: type["BaseModel"], known_models: Union[TypeModelSet, None] = None model: type["BaseModel"], known_models: Union[TypeModelSet, None] = None
) -> TypeModelSet: ) -> TypeModelSet:

8
fastapi/dependencies/utils.py

@ -21,7 +21,6 @@ from fastapi._compat import (
ModelField, ModelField,
RequiredParam, RequiredParam,
Undefined, Undefined,
_regenerate_error_with_loc,
copy_field_info, copy_field_info,
create_body_model, create_body_model,
evaluate_forwardref, evaluate_forwardref,
@ -718,12 +717,7 @@ def _validate_value_with_model_field(
return None, [get_missing_field_error(loc=loc)] return None, [get_missing_field_error(loc=loc)]
else: else:
return deepcopy(field.default), [] return deepcopy(field.default), []
v_, errors_ = field.validate(value, values, loc=loc) return 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_, []
def _is_json_field(field: ModelField) -> bool: def _is_json_field(field: ModelField) -> bool:

8
fastapi/openapi/utils.py

@ -9,11 +9,14 @@ from fastapi import routing
from fastapi._compat import ( from fastapi._compat import (
ModelField, ModelField,
Undefined, Undefined,
get_compat_model_name_map,
get_definitions, get_definitions,
get_schema_from_model_field, get_schema_from_model_field,
lenient_issubclass, lenient_issubclass,
) )
from fastapi._compat.v2 import (
get_flat_models_from_fields,
get_model_name_map,
)
from fastapi.datastructures import DefaultPlaceholder from fastapi.datastructures import DefaultPlaceholder
from fastapi.dependencies.models import Dependant from fastapi.dependencies.models import Dependant
from fastapi.dependencies.utils import ( from fastapi.dependencies.utils import (
@ -512,7 +515,8 @@ def get_openapi(
webhook_paths: dict[str, dict[str, Any]] = {} webhook_paths: dict[str, dict[str, Any]] = {}
operation_ids: set[str] = set() operation_ids: set[str] = set()
all_fields = get_fields_from_routes(list(routes or []) + list(webhooks or [])) 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( field_mapping, definitions = get_definitions(
fields=all_fields, fields=all_fields,
model_name_map=model_name_map, model_name_map=model_name_map,

22
fastapi/routing.py

@ -59,7 +59,6 @@ from fastapi.exceptions import (
) )
from fastapi.types import DecoratedCallable, IncEx from fastapi.types import DecoratedCallable, IncEx
from fastapi.utils import ( from fastapi.utils import (
create_cloned_field,
create_model_field, create_model_field,
generate_unique_id, generate_unique_id,
get_value_or_default, get_value_or_default,
@ -278,15 +277,12 @@ async def serialize_response(
endpoint_ctx: Optional[EndpointContext] = None, endpoint_ctx: Optional[EndpointContext] = None,
) -> Any: ) -> Any:
if field: if field:
errors = []
if is_coroutine: if is_coroutine:
value, errors_ = field.validate(response_content, {}, loc=("response",)) value, errors = field.validate(response_content, {}, loc=("response",))
else: else:
value, errors_ = await run_in_threadpool( value, errors = await run_in_threadpool(
field.validate, response_content, {}, loc=("response",) field.validate, response_content, {}, loc=("response",)
) )
if isinstance(errors_, list):
errors.extend(errors_)
if errors: if errors:
ctx = endpoint_ctx or EndpointContext() ctx = endpoint_ctx or EndpointContext()
raise ResponseValidationError( raise ResponseValidationError(
@ -652,20 +648,8 @@ class APIRoute(routing.Route):
type_=self.response_model, type_=self.response_model,
mode="serialization", 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: else:
self.response_field = None # type: ignore self.response_field = None # type: ignore
self.secure_cloned_response_field = None
self.dependencies = list(dependencies or []) self.dependencies = list(dependencies or [])
self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "") self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "")
# if a "form feed" character (page break) is found in the description text, # 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, body_field=self.body_field,
status_code=self.status_code, status_code=self.status_code,
response_class=self.response_class, 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_include=self.response_model_include,
response_model_exclude=self.response_model_exclude, response_model_exclude=self.response_model_exclude,
response_model_by_alias=self.response_model_by_alias, response_model_by_alias=self.response_model_by_alias,

16
fastapi/utils.py

@ -1,13 +1,11 @@
import re import re
import warnings import warnings
from collections.abc import MutableMapping
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
Optional, Optional,
Union, Union,
) )
from weakref import WeakKeyDictionary
import fastapi import fastapi
from fastapi._compat import ( from fastapi._compat import (
@ -21,7 +19,6 @@ from fastapi._compat import (
) )
from fastapi.datastructures import DefaultPlaceholder, DefaultType from fastapi.datastructures import DefaultPlaceholder, DefaultType
from fastapi.exceptions import FastAPIDeprecationWarning, PydanticV1NotSupportedError from fastapi.exceptions import FastAPIDeprecationWarning, PydanticV1NotSupportedError
from pydantic import BaseModel
from pydantic.fields import FieldInfo from pydantic.fields import FieldInfo
from typing_extensions import Literal from typing_extensions import Literal
@ -30,11 +27,6 @@ from ._compat import v2
if TYPE_CHECKING: # pragma: nocover if TYPE_CHECKING: # pragma: nocover
from .routing import APIRoute 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: def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool:
if status_code is None: if status_code is None:
@ -97,14 +89,6 @@ def create_model_field(
) from None ) 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( def generate_operation_id_for_path(
*, name: str, path: str, method: str *, name: str, path: str, method: str
) -> str: # pragma: nocover ) -> str: # pragma: nocover

Loading…
Cancel
Save