Browse Source

Pydantic v2 migration, initial implementation (#9500)

* WIP

*  Add compat layer, for Pydantic v1 and v2

*  Re-export Pydantic needed internals from compat, to later patch them for v1

* ♻️ Refactor internals to use new compatibility layers and run with Pydantic v2

* 📝 Update examples to run with Pydantic v2

*  Update tests to use Pydantic v2

* 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks

*  Temporarily disable Peewee tests, afterwards I'll enable them only for Pydantic v1

* 🐛 Fix JSON Schema generation and OpenAPI ref template

* 🐛 Fix model field creation with defaults from Pydantic v2

* 🐛 Fix body field creation, with new FieldInfo

*  Use and check new ResponseValidationError for server validation errors

*  Fix test_schema_extra_examples tests with ResponseValidationError

*  Add dirty-equals to tests for compatibility with Pydantic v1 and v2

*  Add util to regenerate errors with custom loc

*  Generate validation errors with loc

*  Update tests for compatibility with Pydantic v1 and v2

*  Update tests for Pydantic v2 in tests/test_filter_pydantic_sub_model.py

*  Refactor tests in tests/test_dependency_overrides.py for Pydantic v2, separate parameterized into independent tests to use insert_assert

*  Refactor OpenAPI test for tests/test_infer_param_optionality.py for consistency, and make it compatible with Pydantic v1 and v2

*  Update tests for tests/test_multi_query_errors.py for Pydantic v1 and v2

*  Update tests for tests/test_multi_body_errors.py for Pydantic v1 and v2

*  Update tests for tests/test_multi_body_errors.py for Pydantic v1 and v2

* 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks

* ♻️ Refactor tests for tests/test_path.py to inline pytest parameters, to make it easier to make them compatible with Pydantic v2

*  Refactor and udpate tests for tests/test_path.py for Pydantic v1 and v2

* ♻️ Refactor and update tests for tests/test_query.py with compatibility for Pydantic v1 and v2

*  Fix test with optional field without default None

*  Update tests for compatibility with Pydantic v2

*  Update tutorial tests for Pydantic v2

* ♻️ Update OAuth2 dependencies for Pydantic v2

* ♻️ Refactor str check when checking for sequence types

* ♻️ Rename regex to pattern to keep in sync with Pydantic v2

* ♻️ Refactor _compat.py, start moving conditional imports and declarations to specifics of Pydantic v1 or v2

*  Update tests for OAuth2 security optional

*  Refactor tests for OAuth2 optional for Pydantic v2

*  Refactor tests for OAuth2 security for compatibility with Pydantic v2

* 🐛 Fix location in compat layer for Pydantic v2 ModelField

*  Refactor tests for Pydantic v2 in tests/test_tutorial/test_bigger_applications/test_main_an_py39.py

* 🐛 Add missing markers in Python 3.9 tests

*  Refactor tests for bigger apps for consistency with annotated ones and with support for Pydantic v2

* 🐛 Fix jsonable_encoder with new Pydantic v2 data types and Url

* 🐛 Fix invalid JSON error for compatibility with Pydantic v2

*  Update tests for behind_a_proxy for Pydantic v2

*  Update tests for tests/test_tutorial/test_body/test_tutorial001_py310.py for Pydantic v2

*  Update tests for tests/test_tutorial/test_body/test_tutorial001.py with Pydantic v2 and consistency with Python 3.10 tests

*  Fix tests for tutorial/body_fields for Pydantic v2

*  Refactor tests for tutorial/body_multiple_params with Pydantic v2

*  Update tests for tutorial/body_nested_models for Pydantic v2

*  Update tests for tutorial/body_updates for Pydantic v2

*  Update test for tutorial/cookie_params for Pydantic v2

*  Fix tests for tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py for Pydantic v2

*  Update tests for tutorial/dataclasses for Pydantic v2

*  Update tests for tutorial/dependencies for Pydantic v2

*  Update tests for tutorial/extra_data_types for Pydantic v2

*  Update tests for tutorial/handling_errors for Pydantic v2

*  Fix test markers for Python 3.9

*  Update tests for tutorial/header_params for Pydantic v2

*  Update tests for Pydantic v2 in tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py

*  Fix extra tests for Pydantic v2

*  Refactor test for parameters, to later fix Pydantic v2

*  Update tests for tutorial/query_params for Pydantic v2

* ♻️ Update examples in docs to use new pattern instead of the old regex

*  Fix several tests for Pydantic v2

*  Update and fix test for ResponseValidationError

* 🐛 Fix check for sequences vs scalars, include bytes as scalar

* 🐛 Fix check for complex data types, include UploadFile

* 🐛 Add list to sequence annotation types

* 🐛 Fix checks for uploads and add utils to find if an annotation is an upload (or bytes)

*  Add UnionType and NoneType to compat layer

*  Update tests for request_files for compatibility with Pydantic v2 and consistency with other tests

*  Fix testsw for request_forms for Pydantic v2

*  Fix tests for request_forms_and_files for Pydantic v2

*  Fix tests in tutorial/security for compatibility with Pydantic v2

* ⬆️ Upgrade required version of email_validator

*  Fix tests for params repr

*  Add Pydantic v2 pytest markers

* Use match_pydantic_error_url

* 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks

* Use field_serializer instead of encoders in some tests

* Show Undefined as ... in repr

* Mark custom encoders test with xfail

* Update test to reflect new serialization of Decimal as str

* Use `model_validate` instead of `from_orm`

* Update JSON schema to reflect required nullable

* Add dirty-equals to pyproject.toml

* Fix locs and error creation for use with pydantic 2.0a4

* Use the type adapter for serialization. This is hacky.

* 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks

*  Refactor test_multi_body_errors for compatibility with Pydantic v1 and v2

*  Refactor test_custom_encoder for Pydantic v1 and v2

*  Set input to None for now, for compatibility with current tests

* 🐛 Fix passing serialization params to model field when handling the response

* ♻️ Refactor exceptions to not depend on Pydantic ValidationError class

* ♻️ Revert/refactor params to simplify repr

*  Tweak tests for custom class encoders for Pydantic v1 and v2

*  Tweak tests for jsonable_encoder for Pydantic v1 and v2

*  Tweak test for compatibility with Pydantic v1 and v2

* 🐛 Fix filtering data with subclasses

* 🐛 Workaround examples in OpenAPI schema

*  Add skip marker for SQL tutorial, needs to be updated either way

*  Update test for broken JSON

*  Fix test for broken JSON

*  Update tests for timedeltas

*  Fix test for plain text validation errors

*  Add markers for Pydantic v1 exclusive tests (for now)

*  Update test for path_params with enums for compatibility with Pydantic v1 and v2

*  Update tests for extra examples in OpenAPI

*  Fix tests for response_model with compatibility with Pydantic v1 and v2

* 🐛 Fix required double serialization for different types of models

*  Fix tests for response model with compatibility with new Pydantic v2

* 🐛 Import Undefined from compat layer

*  Fix tests for response_model for Pydantic v2

*  Fix tests for schema_extra for Pydantic v2

*  Add markers and update tests for Pydantic v2

* 💡 Comment out logic for double encoding that breaks other usecases

*  Update errors for int parsing

* ♻️ Refactor re-enabling compatibility for Pydantic v1

* ♻️ Refactor OpenAPI utils to re-enable support for Pydantic v1

* ♻️ Refactor dependencies/utils and _compat for compatibility with Pydantic v1

* 🐛 Fix and tweak compatibility with Pydantic v1 and v2 in dependencies/utils

*  Tweak tests and examples for Pydantic v1

* ♻️ Tweak call to ModelField.validate for compatibility with Pydantic v1

*  Use new global override TypeAdapter from_attributes

*  Update tests after updating from_attributes

* 🔧 Update pytest config to avoid collecting tests from docs, useful for editor-integrated tests

*  Add test for data filtering, including inheritance and models in fields or lists of models

* ♻️ Make OpenAPI models compatible with both Pydantic v1 and v2

* ♻️ Fix compatibility for Pydantic v1 and v2 in jsonable_encoder

* ♻️ Fix compatibility in params with Pydantic v1 and v2

* ♻️ Fix compatibility when creating a FieldInfo in Pydantic v1 and v2 in utils.py

* ♻️ Fix generation of flat_models and JSON Schema definitions in _compat.py for Pydantic v1 and v2

* ♻️ Update handling of ErrorWrappers for Pydantic v1

* ♻️ Refactor checks and handling of types an sequences

* ♻️ Refactor and cleanup comments with compatibility for Pydantic v1 and v2

* ♻️ Update UploadFile for compatibility with both Pydantic v1 and v2

* 🔥 Remove commented out unneeded code

* 🐛 Fix mock of get_annotation_from_field_info for Pydantic v1 and v2

* 🐛 Fix params with compatibility for Pydantic v1 and v2, with schemas and new pattern vs regex

* 🐛 Fix check if field is sequence for Pydantic v1

*  Fix tests for custom_schema_fields, for compatibility with Pydantic v1 and v2

*  Simplify and fix tests for jsonable_encoder with compatibility for Pydantic v1 and v2

*  Fix tests for orm_mode with Pydantic v1 and compatibility with Pydantic v2

* ♻️ Refactor logic for normalizing Pydantic v1 ErrorWrappers

* ♻️ Workaround for params with examples, before defining what to deprecate in Pydantic v1 and v2 for examples with JSON Schema vs OpenAPI

*  Fix tests for Pydantic v1 and v2 for response_by_alias

*  Fix test for schema_extra with compatibility with Pydantic v1 and v2

* ♻️ Tweak error regeneration with loc

* ♻️ Update error handling and serializationwith compatibility for Pydantic v1 and v2

* ♻️ Re-enable custom encoders for Pydantic v1

* ♻️ Update ErrorWrapper reserialization in Pydantic v1, do it outside of FastAPI ValidationExceptions

*  Update test for filter_submodel, re-structure to simplify testing while keeping division of Pydantic v1 and v2

*  Refactor Pydantic v1 only test that requires modifying environment variables

* 🔥 Update test for plaintext error responses, for Pydantic v1 and v2

* ️ Revert changes in DB tutorial to use Pydantic v1 (the new guide will have SQLModel)

*  Mark current SQL DB tutorial tests as Pydantic only

* ♻️ Update datastructures for compatibility with Pydantic v1, not requiring pydantic-core

* ♻️ Update encoders.py for compatibility with Pydantic v1

* ️ Revert changes to Peewee, the docs for that are gonna live in a new HowTo section, not in the main tutorials

* ♻️ Simplify response body kwargs generation

* 🔥 Clean up comments

* 🔥 Clean some tests and comments

*  Refactor tests to match new Pydantic error string URLs

*  Refactor tests for recursive models for Pydantic v1 and v2

*  Update tests for Peewee, re-enable, Pydantic-v1-only

* ♻️ Update FastAPI params to take regex and pattern arguments

* ️ Revert tutorial examples for pattern, it will be done in a subsequent PR

* ️ Revert changes in schema extra examples, it will be added later in a docs-specific PR

* 💡 Add TODO comment to document str validations with pattern

* 🔥 Remove unneeded comment

* 📌 Upgrade Pydantic pin dependency

* ⬆️ Upgrade email_validator dependency

* 🐛 Tweak type annotations in _compat.py

* 🔇 Tweak mypy errors for compat, for Pydantic v1 re-imports

* 🐛 Tweak and fix type annotations

*  Update requirements-test.txt, re-add dirty-equals

* 🔥 Remove unnecessary config

* 🐛 Tweak type annotations

* 🔥 Remove unnecessary type in dependencies/utils.py

* 💡 Update comment in routing.py

---------

Co-authored-by: David Montague <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
pull/9707/head
Sebastián Ramírez 2 years ago
committed by GitHub
parent
commit
bd32fecaf6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      docs_src/extra_models/tutorial003.py
  2. 4
      docs_src/extra_models/tutorial003_py310.py
  3. 614
      fastapi/_compat.py
  4. 43
      fastapi/applications.py
  5. 35
      fastapi/datastructures.py
  6. 2
      fastapi/dependencies/models.py
  7. 205
      fastapi/dependencies/utils.py
  8. 103
      fastapi/encoders.py
  9. 28
      fastapi/exceptions.py
  10. 1
      fastapi/openapi/constants.py
  11. 216
      fastapi/openapi/models.py
  12. 101
      fastapi/openapi/utils.py
  13. 16
      fastapi/param_functions.py
  14. 65
      fastapi/params.py
  15. 125
      fastapi/routing.py
  16. 31
      fastapi/security/oauth2.py
  17. 10
      fastapi/types.py
  18. 121
      fastapi/utils.py
  19. 5
      pyproject.toml
  20. 4
      requirements-tests.txt
  21. 26
      tests/test_additional_responses_custom_model_in_callback.py
  22. 49
      tests/test_annotated.py
  23. 23
      tests/test_application.py
  24. 15
      tests/test_custom_schema_fields.py
  25. 53
      tests/test_datetime_custom_encoder.py
  26. 35
      tests/test_dependency_duplicates.py
  27. 666
      tests/test_dependency_overrides.py
  28. 10
      tests/test_extra_routes.py
  29. 0
      tests/test_filter_pydantic_sub_model/__init__.py
  30. 35
      tests/test_filter_pydantic_sub_model/app_pv1.py
  31. 52
      tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py
  32. 182
      tests/test_filter_pydantic_sub_model_pv2.py
  33. 277
      tests/test_infer_param_optionality.py
  34. 86
      tests/test_inherited_custom_class.py
  35. 139
      tests/test_jsonable_encoder.py
  36. 160
      tests/test_multi_body_errors.py
  37. 55
      tests/test_multi_query_errors.py
  38. 21
      tests/test_openapi_query_parameter_extension.py
  39. 15
      tests/test_openapi_servers.py
  40. 135
      tests/test_params_repr.py
  41. 1362
      tests/test_path.py
  42. 460
      tests/test_query.py
  43. 83
      tests/test_read_with_orm_mode.py
  44. 1
      tests/test_request_body_parameters_media_type.py
  45. 28
      tests/test_response_by_alias.py
  46. 16
      tests/test_response_model_as_return_annotation.py
  47. 79
      tests/test_response_model_data_filter.py
  48. 81
      tests/test_response_model_data_filter_no_inheritance.py
  49. 113
      tests/test_schema_extra_examples.py
  50. 209
      tests/test_security_oauth2.py
  51. 209
      tests/test_security_oauth2_optional.py
  52. 209
      tests/test_security_oauth2_optional_description.py
  53. 2
      tests/test_skip_defaults.py
  54. 43
      tests/test_sub_callbacks.py
  55. 91
      tests/test_tuples.py
  56. 12
      tests/test_tutorial/test_additional_responses/test_tutorial002.py
  57. 12
      tests/test_tutorial/test_additional_responses/test_tutorial004.py
  58. 4
      tests/test_tutorial/test_async_sql_databases/test_tutorial001.py
  59. 18
      tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py
  60. 18
      tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py
  61. 476
      tests/test_tutorial/test_bigger_applications/test_main.py
  62. 476
      tests/test_tutorial/test_bigger_applications/test_main_an.py
  63. 470
      tests/test_tutorial/test_bigger_applications/test_main_an_py39.py
  64. 459
      tests/test_tutorial/test_body/test_tutorial001.py
  65. 432
      tests/test_tutorial/test_body/test_tutorial001_py310.py
  66. 153
      tests/test_tutorial/test_body_fields/test_tutorial001.py
  67. 153
      tests/test_tutorial/test_body_fields/test_tutorial001_an.py
  68. 145
      tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py
  69. 145
      tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py
  70. 145
      tests/test_tutorial/test_body_fields/test_tutorial001_py310.py
  71. 147
      tests/test_tutorial/test_body_multiple_params/test_tutorial001.py
  72. 147
      tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py
  73. 140
      tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py
  74. 140
      tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py
  75. 140
      tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py
  76. 250
      tests/test_tutorial/test_body_multiple_params/test_tutorial003.py
  77. 250
      tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py
  78. 242
      tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py
  79. 242
      tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py
  80. 242
      tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py
  81. 50
      tests/test_tutorial/test_body_nested_models/test_tutorial009.py
  82. 35
      tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py
  83. 49
      tests/test_tutorial/test_body_updates/test_tutorial001.py
  84. 34
      tests/test_tutorial/test_body_updates/test_tutorial001_py310.py
  85. 34
      tests/test_tutorial/test_body_updates/test_tutorial001_py39.py
  86. 26
      tests/test_tutorial/test_conditional_openapi/test_tutorial001.py
  87. 12
      tests/test_tutorial/test_cookie_params/test_tutorial001.py
  88. 12
      tests/test_tutorial/test_cookie_params/test_tutorial001_an.py
  89. 12
      tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py
  90. 12
      tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py
  91. 12
      tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py
  92. 43
      tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py
  93. 57
      tests/test_tutorial/test_dataclasses/test_tutorial001.py
  94. 47
      tests/test_tutorial/test_dataclasses/test_tutorial002.py
  95. 33
      tests/test_tutorial/test_dataclasses/test_tutorial003.py
  96. 23
      tests/test_tutorial/test_dependencies/test_tutorial001.py
  97. 23
      tests/test_tutorial/test_dependencies/test_tutorial001_an.py
  98. 23
      tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py
  99. 23
      tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py
  100. 23
      tests/test_tutorial/test_dependencies/test_tutorial001_py310.py

4
docs_src/extra_models/tutorial003.py

@ -12,11 +12,11 @@ class BaseItem(BaseModel):
class CarItem(BaseItem): class CarItem(BaseItem):
type = "car" type: str = "car"
class PlaneItem(BaseItem): class PlaneItem(BaseItem):
type = "plane" type: str = "plane"
size: int size: int

4
docs_src/extra_models/tutorial003_py310.py

@ -12,11 +12,11 @@ class BaseItem(BaseModel):
class CarItem(BaseItem): class CarItem(BaseItem):
type = "car" type: str = "car"
class PlaneItem(BaseItem): class PlaneItem(BaseItem):
type = "plane" type: str = "plane"
size: int size: int

614
fastapi/_compat.py

@ -0,0 +1,614 @@
from collections import deque
from copy import copy
from dataclasses import dataclass, is_dataclass
from enum import Enum
from typing import (
Any,
Callable,
Deque,
Dict,
FrozenSet,
List,
Mapping,
Sequence,
Set,
Tuple,
Type,
Union,
)
from fastapi.exceptions import RequestErrorModel
from fastapi.types import IncEx, ModelNameMap, UnionType
from pydantic import BaseModel, create_model
from pydantic.version import VERSION as PYDANTIC_VERSION
from starlette.datastructures import UploadFile
from typing_extensions import Annotated, Literal, get_args, get_origin
PYDANTIC_V2 = PYDANTIC_VERSION.startswith("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())
if PYDANTIC_V2:
from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError
from pydantic import TypeAdapter
from pydantic import ValidationError as ValidationError
from pydantic._internal._fields import Undefined as Undefined
from pydantic._internal._fields import _UndefinedType
from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-defined]
GetJsonSchemaHandler as GetJsonSchemaHandler,
)
from pydantic._internal._typing_extra import eval_type_lenient
from pydantic._internal._utils import lenient_issubclass as lenient_issubclass
from pydantic.fields import FieldInfo
from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema
from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue
from pydantic_core import CoreSchema as CoreSchema
from pydantic_core import ErrorDetails
from pydantic_core import MultiHostUrl as MultiHostUrl
from pydantic_core import Url as Url
from pydantic_core.core_schema import (
general_plain_validator_function as general_plain_validator_function,
)
Required = Undefined
UndefinedType = _UndefinedType
evaluate_forwardref = eval_type_lenient
Validator = Any
class BaseConfig:
pass
class ErrorWrapper(Exception):
pass
@dataclass
class ModelField:
field_info: FieldInfo
name: str
@property
def alias(self) -> str:
a = self.field_info.alias
return a if a is not None else self.name
@property
def required(self) -> bool:
return self.field_info.is_required()
@property
def default(self) -> Any:
return self.get_default()
@property
def type_(self) -> Any:
return self.field_info.annotation
def __post_init__(self) -> None:
self._type_adapter: TypeAdapter[Any] = TypeAdapter(
Annotated[self.field_info.annotation, self.field_info]
)
def get_default(self) -> Any:
if self.field_info.is_required():
return Undefined
return self.field_info.get_default(call_default_factory=True)
def validate(
self,
value: Any,
values: Dict[str, Any] = {}, # noqa: B006
*,
loc: Union[Tuple[Union[int, str], ...], str] = "",
) -> tuple[Any, Union[List[ValidationError], None]]:
try:
return (
self._type_adapter.validate_python(value, from_attributes=True),
None,
)
except ValidationError as exc:
if isinstance(loc, tuple):
use_loc = loc
elif loc == "":
use_loc = ()
else:
use_loc = (loc,)
return None, _regenerate_error_with_loc(
errors=exc.errors(), loc_prefix=use_loc
)
def serialize(
self,
value: Any,
*,
mode: Literal["json", "python"] = "json",
include: Union[IncEx, None] = None,
exclude: Union[IncEx, None] = None,
by_alias: bool = True,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
) -> Any:
# What calls this code passes a value that already called
# self._type_adapter.validate_python(value)
return self._type_adapter.dump_python(
value,
mode=mode,
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
def __hash__(self) -> int:
# Each ModelField is unique for our purposes, to allow making a dict from
# ModelField to its JSON Schema.
return id(self)
def get_model_definitions(
*,
flat_models: Set[Union[Type[BaseModel], Type[Enum]]],
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
) -> Dict[str, Any]:
return {}
def get_annotation_from_field_info(
annotation: Any, field_info: FieldInfo, field_name: str
) -> Any:
return annotation
def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
return errors # type: ignore[return-value]
else:
from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX
from pydantic import AnyUrl as Url # noqa: F401
from pydantic import ( # type: ignore[assignment]
BaseConfig as BaseConfig, # noqa: F401
)
from pydantic import ValidationError as ValidationError # noqa: F401
from pydantic.class_validators import ( # type: ignore[no-redef]
Validator as Validator, # noqa: F401
)
from pydantic.error_wrappers import ( # type: ignore[no-redef]
ErrorWrapper as ErrorWrapper, # noqa: F401
)
from pydantic.errors import MissingError
from pydantic.fields import ( # type: ignore[attr-defined]
SHAPE_FROZENSET,
SHAPE_LIST,
SHAPE_SEQUENCE,
SHAPE_SET,
SHAPE_SINGLETON,
SHAPE_TUPLE,
SHAPE_TUPLE_ELLIPSIS,
)
from pydantic.fields import FieldInfo as FieldInfo
from pydantic.fields import ( # type: ignore[no-redef,attr-defined]
ModelField as ModelField, # noqa: F401
)
from pydantic.fields import ( # type: ignore[no-redef,attr-defined]
Required as Required, # noqa: F401
)
from pydantic.fields import ( # type: ignore[no-redef,attr-defined]
Undefined as Undefined,
)
from pydantic.fields import ( # type: ignore[no-redef, attr-defined]
UndefinedType as UndefinedType, # noqa: F401
)
from pydantic.networks import ( # type: ignore[no-redef]
MultiHostDsn as MultiHostUrl, # noqa: F401
)
from pydantic.schema import (
field_schema,
get_flat_models_from_fields,
get_model_name_map,
model_process_schema,
)
from pydantic.schema import ( # type: ignore[no-redef] # noqa: F401
get_annotation_from_field_info as get_annotation_from_field_info,
)
from pydantic.typing import ( # type: ignore[no-redef]
evaluate_forwardref as evaluate_forwardref, # noqa: F401
)
from pydantic.utils import ( # type: ignore[no-redef]
lenient_issubclass as lenient_issubclass, # noqa: F401
)
ErrorDetails = Dict[str, Any] # type: ignore[assignment,misc]
GetJsonSchemaHandler = Any # type: ignore[assignment,misc]
JsonSchemaValue = Dict[str, Any] # type: ignore[misc]
CoreSchema = Any # type: ignore[assignment,misc]
sequence_shapes = {
SHAPE_LIST,
SHAPE_SET,
SHAPE_FROZENSET,
SHAPE_TUPLE,
SHAPE_SEQUENCE,
SHAPE_TUPLE_ELLIPSIS,
}
sequence_shape_to_type = {
SHAPE_LIST: list,
SHAPE_SET: set,
SHAPE_TUPLE: tuple,
SHAPE_SEQUENCE: list,
SHAPE_TUPLE_ELLIPSIS: list,
}
@dataclass
class GenerateJsonSchema: # type: ignore[no-redef]
ref_template: str
class PydanticSchemaGenerationError(Exception): # type: ignore[no-redef]
pass
def general_plain_validator_function( # type: ignore[misc]
function: Callable[..., Any],
*,
ref: Union[str, None] = None,
metadata: Any = None,
serialization: Any = None,
) -> Any:
return {}
def get_model_definitions(
*,
flat_models: Set[Union[Type[BaseModel], Type[Enum]]],
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
) -> Dict[str, Any]:
definitions: Dict[str, Dict[str, Any]] = {}
for model in flat_models:
m_schema, m_definitions, m_nested_models = model_process_schema(
model, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)
definitions.update(m_definitions)
model_name = model_name_map[model]
if "description" in m_schema:
m_schema["description"] = m_schema["description"].split("\f")[0]
definitions[model_name] = m_schema
return definitions
def is_pv1_scalar_field(field: ModelField) -> bool:
from fastapi import params
field_info = field.field_info
if not (
field.shape == SHAPE_SINGLETON # type: ignore[attr-defined]
and not lenient_issubclass(field.type_, BaseModel)
and not lenient_issubclass(field.type_, dict)
and not field_annotation_is_sequence(field.type_)
and not is_dataclass(field.type_)
and not isinstance(field_info, params.Body)
):
return False
if field.sub_fields: # type: ignore[attr-defined]
if not all(
is_pv1_scalar_field(f)
for f in field.sub_fields # type: ignore[attr-defined]
):
return False
return True
def is_pv1_scalar_sequence_field(field: ModelField) -> bool:
if (field.shape in sequence_shapes) and not lenient_issubclass( # type: ignore[attr-defined]
field.type_, BaseModel
):
if field.sub_fields is not None: # type: ignore[attr-defined]
for sub_field in field.sub_fields: # type: ignore[attr-defined]
if not is_pv1_scalar_field(sub_field):
return False
return True
if _annotation_is_sequence(field.type_):
return True
return False
def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
use_errors: List[Any] = []
for error in errors:
if isinstance(error, ErrorWrapper):
new_errors = ValidationError( # type: ignore[call-arg]
errors=[error], model=RequestErrorModel
).errors()
use_errors.extend(new_errors)
elif isinstance(error, list):
use_errors.extend(_normalize_errors(error))
else:
use_errors.append(error)
return use_errors
def _regenerate_error_with_loc(
*, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...]
) -> List[ValidationError]:
updated_loc_errors: List[Any] = [
{**err, "loc": loc_prefix + err.get("loc", ())}
for err in _normalize_errors(errors)
]
return updated_loc_errors
def _model_rebuild(model: Type[BaseModel]) -> None:
if PYDANTIC_V2:
model.model_rebuild()
else:
model.update_forward_refs()
def _model_dump(
model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
) -> Any:
if PYDANTIC_V2:
return model.model_dump(mode=mode, **kwargs)
else:
return model.dict(**kwargs)
def _get_model_config(model: BaseModel) -> Any:
if PYDANTIC_V2:
return model.model_config
else:
return model.__config__ # type: ignore[attr-defined]
def get_schema_from_model_field(
*,
field: ModelField,
schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
) -> Dict[str, Any]:
# This expects that GenerateJsonSchema was already used to generate the definitions
if PYDANTIC_V2:
json_schema = schema_generator.generate_inner(field._type_adapter.core_schema)
if "$ref" not in json_schema:
# TODO remove when deprecating Pydantic v1
# Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207
json_schema[
"title"
] = field.field_info.title or field.alias.title().replace("_", " ")
return json_schema
else:
return field_schema( # type: ignore[no-any-return]
field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)[0]
def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
if PYDANTIC_V2:
return {}
else:
models = get_flat_models_from_fields(fields, known_models=set())
return get_model_name_map(models) # type: ignore[no-any-return]
def get_definitions(
*,
fields: List[ModelField],
schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
) -> Dict[str, Dict[str, Any]]:
if PYDANTIC_V2:
inputs = [
(field, "validation", field._type_adapter.core_schema) for field in fields
]
_, definitions = schema_generator.generate_definitions(inputs=inputs) # type: ignore[arg-type]
return definitions # type: ignore[return-value]
else:
models = get_flat_models_from_fields(fields, known_models=set())
return get_model_definitions(flat_models=models, model_name_map=model_name_map)
def _annotation_is_sequence(annotation: type[Any] | None) -> bool:
if lenient_issubclass(annotation, (str, bytes)):
return False
return lenient_issubclass(annotation, sequence_types)
def field_annotation_is_sequence(annotation: type[Any] | None) -> bool:
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: Type[Any] | None) -> bool:
return (
lenient_issubclass(annotation, (BaseModel, Mapping, UploadFile))
or _annotation_is_sequence(annotation)
or is_dataclass(annotation)
)
def field_annotation_is_complex(annotation: 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))
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:
origin = get_origin(annotation)
if origin is Union or origin is UnionType:
return all(field_annotation_is_scalar(arg) for arg in get_args(annotation))
# 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: 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_scalar_field(field: ModelField) -> bool:
from fastapi import params
if PYDANTIC_V2:
return field_annotation_is_scalar(
field.field_info.annotation
) and not isinstance(field.field_info, params.Body)
else:
return is_pv1_scalar_field(field)
def is_sequence_field(field: ModelField) -> bool:
if PYDANTIC_V2:
return field_annotation_is_sequence(field.field_info.annotation)
else:
return field.shape in sequence_shapes or _annotation_is_sequence(field.type_) # type: ignore[attr-defined]
def is_scalar_sequence_field(field: ModelField) -> bool:
if PYDANTIC_V2:
return field_annotation_is_scalar_sequence(field.field_info.annotation)
else:
return is_pv1_scalar_sequence_field(field)
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: type[Any] | None) -> bool:
origin = get_origin(annotation)
if origin is Union or origin is UnionType:
at_least_one_bytes_sequence = False
for arg in get_args(annotation):
if is_bytes_sequence_annotation(arg):
at_least_one_bytes_sequence = True
continue
return at_least_one_bytes_sequence
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: type[Any] | None) -> bool:
origin = get_origin(annotation)
if origin is Union or origin is UnionType:
at_least_one_bytes_sequence = False
for arg in get_args(annotation):
if is_uploadfile_sequence_annotation(arg):
at_least_one_bytes_sequence = True
continue
return at_least_one_bytes_sequence
return field_annotation_is_sequence(annotation) and all(
is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation)
for sub_annotation in get_args(annotation)
)
def is_bytes_field(field: ModelField) -> bool:
if PYDANTIC_V2:
return is_bytes_or_nonable_bytes_annotation(field.type_)
else:
return lenient_issubclass(field.type_, bytes)
def is_bytes_sequence_field(field: ModelField) -> bool:
if PYDANTIC_V2:
return is_bytes_sequence_annotation(field.type_)
else:
return field.shape in sequence_shapes and lenient_issubclass(field.type_, bytes) # type: ignore[attr-defined]
def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
if PYDANTIC_V2:
return type(field_info).from_annotation(annotation)
else:
return copy(field_info)
def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
if PYDANTIC_V2:
origin_type = (
get_origin(field.field_info.annotation) or field.field_info.annotation
)
assert issubclass(origin_type, sequence_types) # type: ignore[arg-type]
return sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return]
else:
return sequence_shape_to_type[field.shape](value) # type: ignore[no-any-return,attr-defined]
def get_missing_field_error(loc: Tuple[str, ...]) -> ValidationError:
if PYDANTIC_V2:
error = ValidationError.from_exception_data(
"Field required", [{"type": "missing", "loc": loc, "input": {}}]
).errors()[0]
error["input"] = None
return error # type: ignore[return-value]
else:
missing_field_error = ErrorWrapper(MissingError(), loc=loc) # type: ignore[call-arg]
new_error = ValidationError([missing_field_error], RequestErrorModel)
return new_error.errors()[0] # type: ignore[return-value]
def create_body_model(
*, fields: Sequence[ModelField], model_name: str
) -> Type[BaseModel]:
if PYDANTIC_V2:
field_params = {f.name: (f.field_info.annotation, f.field_info) for f in fields}
BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload]
return BodyModel
else:
BodyModel = create_model(model_name)
for f in fields:
BodyModel.__fields__[f.name] = f # type: ignore[index]
return BodyModel

43
fastapi/applications.py

@ -15,7 +15,6 @@ from typing import (
from fastapi import routing from fastapi import routing
from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.datastructures import Default, DefaultPlaceholder
from fastapi.encoders import DictIntStrAny, SetIntStr
from fastapi.exception_handlers import ( from fastapi.exception_handlers import (
http_exception_handler, http_exception_handler,
request_validation_exception_handler, request_validation_exception_handler,
@ -31,7 +30,7 @@ from fastapi.openapi.docs import (
) )
from fastapi.openapi.utils import get_openapi from fastapi.openapi.utils import get_openapi
from fastapi.params import Depends from fastapi.params import Depends
from fastapi.types import DecoratedCallable from fastapi.types import DecoratedCallable, IncEx
from fastapi.utils import generate_unique_id from fastapi.utils import generate_unique_id
from starlette.applications import Starlette from starlette.applications import Starlette
from starlette.datastructures import State from starlette.datastructures import State
@ -297,8 +296,8 @@ class FastAPI(Starlette):
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
methods: Optional[List[str]] = None, methods: Optional[List[str]] = None,
operation_id: Optional[str] = None, operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False, response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,
@ -355,8 +354,8 @@ class FastAPI(Starlette):
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
methods: Optional[List[str]] = None, methods: Optional[List[str]] = None,
operation_id: Optional[str] = None, operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False, response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,
@ -476,8 +475,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
operation_id: Optional[str] = None, operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False, response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,
@ -531,8 +530,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
operation_id: Optional[str] = None, operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False, response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,
@ -586,8 +585,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
operation_id: Optional[str] = None, operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False, response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,
@ -641,8 +640,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
operation_id: Optional[str] = None, operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False, response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,
@ -696,8 +695,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
operation_id: Optional[str] = None, operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False, response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,
@ -751,8 +750,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
operation_id: Optional[str] = None, operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False, response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,
@ -806,8 +805,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
operation_id: Optional[str] = None, operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False, response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,
@ -861,8 +860,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
operation_id: Optional[str] = None, operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False, response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,

35
fastapi/datastructures.py

@ -1,5 +1,12 @@
from typing import Any, Callable, Dict, Iterable, Type, TypeVar from typing import Any, Callable, Dict, Iterable, Type, TypeVar, cast
from fastapi._compat import (
PYDANTIC_V2,
CoreSchema,
GetJsonSchemaHandler,
JsonSchemaValue,
general_plain_validator_function,
)
from starlette.datastructures import URL as URL # noqa: F401 from starlette.datastructures import URL as URL # noqa: F401
from starlette.datastructures import Address as Address # noqa: F401 from starlette.datastructures import Address as Address # noqa: F401
from starlette.datastructures import FormData as FormData # noqa: F401 from starlette.datastructures import FormData as FormData # noqa: F401
@ -21,8 +28,28 @@ class UploadFile(StarletteUploadFile):
return v return v
@classmethod @classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: def _validate(cls, __input_value: Any, _: Any) -> "UploadFile":
field_schema.update({"type": "string", "format": "binary"}) if not isinstance(__input_value, StarletteUploadFile):
raise ValueError(f"Expected UploadFile, received: {type(__input_value)}")
return cast(UploadFile, __input_value)
if not PYDANTIC_V2:
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
field_schema.update({"type": "string", "format": "binary"})
@classmethod
def __get_pydantic_json_schema__(
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
) -> JsonSchemaValue:
return {"type": "string", "format": "binary"}
@classmethod
def __get_pydantic_core_schema__(
cls, source: Type[Any], handler: Callable[[Any], CoreSchema]
) -> CoreSchema:
return general_plain_validator_function(cls._validate)
class DefaultPlaceholder: class DefaultPlaceholder:

2
fastapi/dependencies/models.py

@ -1,7 +1,7 @@
from typing import Any, Callable, List, Optional, Sequence from typing import Any, Callable, List, Optional, Sequence
from fastapi._compat import ModelField
from fastapi.security.base import SecurityBase from fastapi.security.base import SecurityBase
from pydantic.fields import ModelField
class SecurityRequirement: class SecurityRequirement:

205
fastapi/dependencies/utils.py

@ -1,7 +1,6 @@
import dataclasses
import inspect import inspect
from contextlib import contextmanager from contextlib import contextmanager
from copy import copy, deepcopy from copy import deepcopy
from typing import ( from typing import (
Any, Any,
Callable, Callable,
@ -20,6 +19,32 @@ from typing import (
import anyio import anyio
from fastapi import params from fastapi import params
from fastapi._compat import (
PYDANTIC_V2,
ErrorWrapper,
ModelField,
Required,
Undefined,
ValidationError,
_regenerate_error_with_loc,
copy_field_info,
create_body_model,
evaluate_forwardref,
field_annotation_is_scalar,
get_annotation_from_field_info,
get_missing_field_error,
is_bytes_field,
is_bytes_sequence_field,
is_scalar_field,
is_scalar_sequence_field,
is_sequence_field,
is_uploadfile_or_nonable_uploadfile_annotation,
is_uploadfile_sequence_annotation,
lenient_issubclass,
sequence_types,
serialize_sequence_value,
value_is_sequence,
)
from fastapi.concurrency import ( from fastapi.concurrency import (
AsyncExitStack, AsyncExitStack,
asynccontextmanager, asynccontextmanager,
@ -31,50 +56,14 @@ from fastapi.security.base import SecurityBase
from fastapi.security.oauth2 import OAuth2, SecurityScopes from fastapi.security.oauth2 import OAuth2, SecurityScopes
from fastapi.security.open_id_connect_url import OpenIdConnect from fastapi.security.open_id_connect_url import OpenIdConnect
from fastapi.utils import create_response_field, get_path_param_names from fastapi.utils import create_response_field, get_path_param_names
from pydantic import BaseModel, create_model from pydantic.fields import FieldInfo
from pydantic.error_wrappers import ErrorWrapper
from pydantic.errors import MissingError
from pydantic.fields import (
SHAPE_FROZENSET,
SHAPE_LIST,
SHAPE_SEQUENCE,
SHAPE_SET,
SHAPE_SINGLETON,
SHAPE_TUPLE,
SHAPE_TUPLE_ELLIPSIS,
FieldInfo,
ModelField,
Required,
Undefined,
)
from pydantic.schema import get_annotation_from_field_info
from pydantic.typing import evaluate_forwardref, get_args, get_origin
from pydantic.utils import lenient_issubclass
from starlette.background import BackgroundTasks from starlette.background import BackgroundTasks
from starlette.concurrency import run_in_threadpool from starlette.concurrency import run_in_threadpool
from starlette.datastructures import FormData, Headers, QueryParams, UploadFile from starlette.datastructures import FormData, Headers, QueryParams, UploadFile
from starlette.requests import HTTPConnection, Request from starlette.requests import HTTPConnection, Request
from starlette.responses import Response from starlette.responses import Response
from starlette.websockets import WebSocket from starlette.websockets import WebSocket
from typing_extensions import Annotated from typing_extensions import Annotated, get_args, get_origin
sequence_shapes = {
SHAPE_LIST,
SHAPE_SET,
SHAPE_FROZENSET,
SHAPE_TUPLE,
SHAPE_SEQUENCE,
SHAPE_TUPLE_ELLIPSIS,
}
sequence_types = (list, set, tuple)
sequence_shape_to_type = {
SHAPE_LIST: list,
SHAPE_SET: set,
SHAPE_TUPLE: tuple,
SHAPE_SEQUENCE: list,
SHAPE_TUPLE_ELLIPSIS: list,
}
multipart_not_installed_error = ( multipart_not_installed_error = (
'Form data requires "python-multipart" to be installed. \n' 'Form data requires "python-multipart" to be installed. \n'
@ -216,36 +205,6 @@ def get_flat_params(dependant: Dependant) -> List[ModelField]:
) )
def is_scalar_field(field: ModelField) -> bool:
field_info = field.field_info
if not (
field.shape == SHAPE_SINGLETON
and not lenient_issubclass(field.type_, BaseModel)
and not lenient_issubclass(field.type_, sequence_types + (dict,))
and not dataclasses.is_dataclass(field.type_)
and not isinstance(field_info, params.Body)
):
return False
if field.sub_fields:
if not all(is_scalar_field(f) for f in field.sub_fields):
return False
return True
def is_scalar_sequence_field(field: ModelField) -> bool:
if (field.shape in sequence_shapes) and not lenient_issubclass(
field.type_, BaseModel
):
if field.sub_fields is not None:
for sub_field in field.sub_fields:
if not is_scalar_field(sub_field):
return False
return True
if lenient_issubclass(field.type_, sequence_types):
return True
return False
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
signature = inspect.signature(call) signature = inspect.signature(call)
globalns = getattr(call, "__globals__", {}) globalns = getattr(call, "__globals__", {})
@ -364,12 +323,11 @@ def analyze_param(
is_path_param: bool, is_path_param: bool,
) -> Tuple[Any, Optional[params.Depends], Optional[ModelField]]: ) -> Tuple[Any, Optional[params.Depends], Optional[ModelField]]:
field_info = None field_info = None
used_default_field_info = False
depends = None depends = None
type_annotation: Any = Any type_annotation: Any = Any
if ( if (
annotation is not inspect.Signature.empty annotation is not inspect.Signature.empty
and get_origin(annotation) is Annotated # type: ignore[comparison-overlap] and get_origin(annotation) is Annotated
): ):
annotated_args = get_args(annotation) annotated_args = get_args(annotation)
type_annotation = annotated_args[0] type_annotation = annotated_args[0]
@ -384,7 +342,9 @@ def analyze_param(
fastapi_annotation = next(iter(fastapi_annotations), None) fastapi_annotation = next(iter(fastapi_annotations), None)
if isinstance(fastapi_annotation, FieldInfo): if isinstance(fastapi_annotation, FieldInfo):
# Copy `field_info` because we mutate `field_info.default` below. # Copy `field_info` because we mutate `field_info.default` below.
field_info = copy(fastapi_annotation) field_info = copy_field_info(
field_info=fastapi_annotation, annotation=annotation
)
assert field_info.default is Undefined or field_info.default is Required, ( assert field_info.default is Undefined or field_info.default is Required, (
f"`{field_info.__class__.__name__}` default value cannot be set in" f"`{field_info.__class__.__name__}` default value cannot be set in"
f" `Annotated` for {param_name!r}. Set the default value with `=` instead." f" `Annotated` for {param_name!r}. Set the default value with `=` instead."
@ -415,6 +375,8 @@ def analyze_param(
f" together for {param_name!r}" f" together for {param_name!r}"
) )
field_info = value field_info = value
if PYDANTIC_V2:
field_info.annotation = type_annotation
if depends is not None and depends.dependency is None: if depends is not None and depends.dependency is None:
depends.dependency = type_annotation depends.dependency = type_annotation
@ -433,10 +395,15 @@ def analyze_param(
# We might check here that `default_value is Required`, but the fact is that the same # We might check here that `default_value is Required`, but the fact is that the same
# parameter might sometimes be a path parameter and sometimes not. See # parameter might sometimes be a path parameter and sometimes not. See
# `tests/test_infer_param_optionality.py` for an example. # `tests/test_infer_param_optionality.py` for an example.
field_info = params.Path() field_info = params.Path(annotation=type_annotation)
elif is_uploadfile_or_nonable_uploadfile_annotation(
type_annotation
) or is_uploadfile_sequence_annotation(type_annotation):
field_info = params.File(annotation=type_annotation, default=default_value)
elif not field_annotation_is_scalar(annotation=type_annotation):
field_info = params.Body(annotation=type_annotation, default=default_value)
else: else:
field_info = params.Query(default=default_value) field_info = params.Query(annotation=type_annotation, default=default_value)
used_default_field_info = True
field = None field = None
if field_info is not None: if field_info is not None:
@ -450,8 +417,8 @@ def analyze_param(
and getattr(field_info, "in_", None) is None and getattr(field_info, "in_", None) is None
): ):
field_info.in_ = params.ParamTypes.query field_info.in_ = params.ParamTypes.query
annotation = get_annotation_from_field_info( use_annotation = get_annotation_from_field_info(
annotation if annotation is not inspect.Signature.empty else Any, type_annotation,
field_info, field_info,
param_name, param_name,
) )
@ -459,19 +426,15 @@ def analyze_param(
alias = param_name.replace("_", "-") alias = param_name.replace("_", "-")
else: else:
alias = field_info.alias or param_name alias = field_info.alias or param_name
field_info.alias = alias
field = create_response_field( field = create_response_field(
name=param_name, name=param_name,
type_=annotation, type_=use_annotation,
default=field_info.default, default=field_info.default,
alias=alias, alias=alias,
required=field_info.default in (Required, Undefined), required=field_info.default in (Required, Undefined),
field_info=field_info, field_info=field_info,
) )
if used_default_field_info:
if lenient_issubclass(field.type_, UploadFile):
field.field_info = params.File(field_info.default)
elif not is_scalar_field(field=field):
field.field_info = params.Body(field_info.default)
return type_annotation, depends, field return type_annotation, depends, field
@ -554,13 +517,13 @@ async def solve_dependencies(
dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None, dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None,
) -> Tuple[ ) -> Tuple[
Dict[str, Any], Dict[str, Any],
List[ErrorWrapper], List[Any],
Optional[BackgroundTasks], Optional[BackgroundTasks],
Response, Response,
Dict[Tuple[Callable[..., Any], Tuple[str]], Any], Dict[Tuple[Callable[..., Any], Tuple[str]], Any],
]: ]:
values: Dict[str, Any] = {} values: Dict[str, Any] = {}
errors: List[ErrorWrapper] = [] errors: List[Any] = []
if response is None: if response is None:
response = Response() response = Response()
del response.headers["content-length"] del response.headers["content-length"]
@ -674,7 +637,7 @@ async def solve_dependencies(
def request_params_to_args( def request_params_to_args(
required_params: Sequence[ModelField], required_params: Sequence[ModelField],
received_params: Union[Mapping[str, Any], QueryParams, Headers], received_params: Union[Mapping[str, Any], QueryParams, Headers],
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]: ) -> Tuple[Dict[str, Any], List[Any]]:
values = {} values = {}
errors = [] errors = []
for field in required_params: for field in required_params:
@ -688,23 +651,25 @@ def request_params_to_args(
assert isinstance( assert isinstance(
field_info, params.Param field_info, params.Param
), "Params must be subclasses of Param" ), "Params must be subclasses of Param"
loc = (field_info.in_.value, field.alias)
if value is None: if value is None:
if field.required: if field.required:
errors.append( errors.append(get_missing_field_error(loc=loc))
ErrorWrapper(
MissingError(), loc=(field_info.in_.value, field.alias)
)
)
else: else:
values[field.name] = deepcopy(field.default) values[field.name] = deepcopy(field.default)
continue continue
v_, errors_ = field.validate( v_, errors_ = field.validate(value, values, loc=loc)
value, values, loc=(field_info.in_.value, field.alias) if isinstance(errors_, ValidationError):
) new_errors = _regenerate_error_with_loc(
if isinstance(errors_, ErrorWrapper): errors=errors_.errors(), loc_prefix=loc
)
new_error = ValidationError(title=errors_.title, errors=new_errors)
errors.append(new_error)
elif isinstance(errors_, ErrorWrapper):
errors.append(errors_) errors.append(errors_)
elif isinstance(errors_, list): elif isinstance(errors_, list):
errors.extend(errors_) new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
errors.extend(new_errors)
else: else:
values[field.name] = v_ values[field.name] = v_
return values, errors return values, errors
@ -713,9 +678,9 @@ def request_params_to_args(
async def request_body_to_args( async def request_body_to_args(
required_params: List[ModelField], required_params: List[ModelField],
received_body: Optional[Union[Dict[str, Any], FormData]], received_body: Optional[Union[Dict[str, Any], FormData]],
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]: ) -> Tuple[Dict[str, Any], List[ValidationError]]:
values = {} values = {}
errors = [] errors: List[ValidationError] = []
if required_params: if required_params:
field = required_params[0] field = required_params[0]
field_info = field.field_info field_info = field.field_info
@ -733,9 +698,7 @@ async def request_body_to_args(
value: Optional[Any] = None value: Optional[Any] = None
if received_body is not None: if received_body is not None:
if ( if (is_sequence_field(field)) and isinstance(received_body, FormData):
field.shape in sequence_shapes or field.type_ in sequence_types
) and isinstance(received_body, FormData):
value = received_body.getlist(field.alias) value = received_body.getlist(field.alias)
else: else:
try: try:
@ -748,7 +711,7 @@ async def request_body_to_args(
or (isinstance(field_info, params.Form) and value == "") or (isinstance(field_info, params.Form) and value == "")
or ( or (
isinstance(field_info, params.Form) isinstance(field_info, params.Form)
and field.shape in sequence_shapes and is_sequence_field(field)
and len(value) == 0 and len(value) == 0
) )
): ):
@ -759,16 +722,17 @@ async def request_body_to_args(
continue continue
if ( if (
isinstance(field_info, params.File) isinstance(field_info, params.File)
and lenient_issubclass(field.type_, bytes) and is_bytes_field(field)
and isinstance(value, UploadFile) and isinstance(value, UploadFile)
): ):
value = await value.read() value = await value.read()
elif ( elif (
field.shape in sequence_shapes is_bytes_sequence_field(field)
and isinstance(field_info, params.File) and isinstance(field_info, params.File)
and lenient_issubclass(field.type_, bytes) and value_is_sequence(value)
and isinstance(value, sequence_types)
): ):
# For types
assert isinstance(value, sequence_types) # type: ignore[arg-type]
results: List[Union[bytes, str]] = [] results: List[Union[bytes, str]] = []
async def process_fn( async def process_fn(
@ -780,24 +744,19 @@ async def request_body_to_args(
async with anyio.create_task_group() as tg: async with anyio.create_task_group() as tg:
for sub_value in value: for sub_value in value:
tg.start_soon(process_fn, sub_value.read) tg.start_soon(process_fn, sub_value.read)
value = sequence_shape_to_type[field.shape](results) value = serialize_sequence_value(field=field, value=results)
v_, errors_ = field.validate(value, values, loc=loc) v_, errors_ = field.validate(value, values, loc=loc)
if isinstance(errors_, ErrorWrapper): if isinstance(errors_, list):
errors.append(errors_)
elif isinstance(errors_, list):
errors.extend(errors_) errors.extend(errors_)
elif errors_:
errors.append(errors_)
else: else:
values[field.name] = v_ values[field.name] = v_
return values, errors return values, errors
def get_missing_field_error(loc: Tuple[str, ...]) -> ErrorWrapper:
missing_field_error = ErrorWrapper(MissingError(), loc=loc)
return missing_field_error
def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
flat_dependant = get_flat_dependant(dependant) flat_dependant = get_flat_dependant(dependant)
if not flat_dependant.body_params: if not flat_dependant.body_params:
@ -815,12 +774,16 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
for param in flat_dependant.body_params: for param in flat_dependant.body_params:
setattr(param.field_info, "embed", True) # noqa: B010 setattr(param.field_info, "embed", True) # noqa: B010
model_name = "Body_" + name model_name = "Body_" + name
BodyModel: Type[BaseModel] = create_model(model_name) BodyModel = create_body_model(
for f in flat_dependant.body_params: fields=flat_dependant.body_params, model_name=model_name
BodyModel.__fields__[f.name] = f )
required = any(True for f in flat_dependant.body_params if f.required) required = any(True for f in flat_dependant.body_params if f.required)
BodyFieldInfo_kwargs: Dict[str, Any] = {
BodyFieldInfo_kwargs: Dict[str, Any] = {"default": None} "annotation": BodyModel,
"alias": "body",
}
if not required:
BodyFieldInfo_kwargs["default"] = None
if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params): if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params):
BodyFieldInfo: Type[params.Body] = params.File BodyFieldInfo: Type[params.Body] = params.File
elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params): elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params):

103
fastapi/encoders.py

@ -1,15 +1,86 @@
import dataclasses import dataclasses
from collections import defaultdict import datetime
from collections import defaultdict, deque
from decimal import Decimal
from enum import Enum from enum import Enum
from pathlib import PurePath from ipaddress import (
IPv4Address,
IPv4Interface,
IPv4Network,
IPv6Address,
IPv6Interface,
IPv6Network,
)
from pathlib import Path, PurePath
from re import Pattern
from types import GeneratorType from types import GeneratorType
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
from uuid import UUID
from fastapi.types import IncEx
from pydantic import BaseModel from pydantic import BaseModel
from pydantic.json import ENCODERS_BY_TYPE from pydantic.color import Color
from pydantic.networks import NameEmail
from pydantic.types import SecretBytes, SecretStr
SetIntStr = Set[Union[int, str]] from ._compat import PYDANTIC_V2, MultiHostUrl, Url, _model_dump
DictIntStrAny = Dict[Union[int, str], Any]
# Taken from Pydantic v1 as is
def isoformat(o: Union[datetime.date, datetime.time]) -> str:
return o.isoformat()
# Taken from Pydantic v1 as is
def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
"""
Encodes a Decimal as int of there's no exponent, otherwise float
This is useful when we use ConstrainedDecimal to represent Numeric(x,0)
where a integer (but not int typed) is used. Encoding this as a float
results in failed round-tripping between encode and parse.
Our Id type is a prime example of this.
>>> decimal_encoder(Decimal("1.0"))
1.0
>>> decimal_encoder(Decimal("1"))
1
"""
if dec_value.as_tuple().exponent >= 0: # type: ignore[operator]
return int(dec_value)
else:
return float(dec_value)
ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = {
bytes: lambda o: o.decode(),
Color: str,
datetime.date: isoformat,
datetime.datetime: isoformat,
datetime.time: isoformat,
datetime.timedelta: lambda td: td.total_seconds(),
Decimal: decimal_encoder,
Enum: lambda o: o.value,
frozenset: list,
deque: list,
GeneratorType: list,
IPv4Address: str,
IPv4Interface: str,
IPv4Network: str,
IPv6Address: str,
IPv6Interface: str,
IPv6Network: str,
NameEmail: str,
Path: str,
Pattern: lambda o: o.pattern,
SecretBytes: str,
SecretStr: str,
set: list,
UUID: str,
Url: str,
MultiHostUrl: str,
}
def generate_encoders_by_class_tuples( def generate_encoders_by_class_tuples(
@ -28,8 +99,8 @@ encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE)
def jsonable_encoder( def jsonable_encoder(
obj: Any, obj: Any,
include: Optional[Union[SetIntStr, DictIntStrAny]] = None, include: Optional[IncEx] = None,
exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, exclude: Optional[IncEx] = None,
by_alias: bool = True, by_alias: bool = True,
exclude_unset: bool = False, exclude_unset: bool = False,
exclude_defaults: bool = False, exclude_defaults: bool = False,
@ -50,10 +121,15 @@ def jsonable_encoder(
if exclude is not None and not isinstance(exclude, (set, dict)): if exclude is not None and not isinstance(exclude, (set, dict)):
exclude = set(exclude) exclude = set(exclude)
if isinstance(obj, BaseModel): if isinstance(obj, BaseModel):
encoder = getattr(obj.__config__, "json_encoders", {}) # TODO: remove when deprecating Pydantic v1
if custom_encoder: encoders: Dict[Any, Any] = {}
encoder.update(custom_encoder) if not PYDANTIC_V2:
obj_dict = obj.dict( encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined]
if custom_encoder:
encoders.update(custom_encoder)
obj_dict = _model_dump(
obj,
mode="json",
include=include, include=include,
exclude=exclude, exclude=exclude,
by_alias=by_alias, by_alias=by_alias,
@ -67,7 +143,8 @@ def jsonable_encoder(
obj_dict, obj_dict,
exclude_none=exclude_none, exclude_none=exclude_none,
exclude_defaults=exclude_defaults, exclude_defaults=exclude_defaults,
custom_encoder=encoder, # TODO: remove when deprecating Pydantic v1
custom_encoder=encoders,
sqlalchemy_safe=sqlalchemy_safe, sqlalchemy_safe=sqlalchemy_safe,
) )
if dataclasses.is_dataclass(obj): if dataclasses.is_dataclass(obj):

28
fastapi/exceptions.py

@ -1,7 +1,6 @@
from typing import Any, Dict, Optional, Sequence, Type from typing import Any, Dict, Optional, Sequence, Type
from pydantic import BaseModel, ValidationError, create_model from pydantic import BaseModel, create_model
from pydantic.error_wrappers import ErrorList
from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.exceptions import WebSocketException as WebSocketException # noqa: F401 from starlette.exceptions import WebSocketException as WebSocketException # noqa: F401
@ -26,12 +25,25 @@ class FastAPIError(RuntimeError):
""" """
class RequestValidationError(ValidationError): class ValidationException(Exception):
def __init__(self, errors: Sequence[ErrorList], *, body: Any = None) -> None: def __init__(self, errors: Sequence[Any]) -> None:
self._errors = errors
def errors(self) -> Sequence[Any]:
return self._errors
class RequestValidationError(ValidationException):
def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
super().__init__(errors)
self.body = body self.body = body
super().__init__(errors, RequestErrorModel)
class WebSocketRequestValidationError(ValidationError): class WebSocketRequestValidationError(ValidationException):
def __init__(self, errors: Sequence[ErrorList]) -> None: pass
super().__init__(errors, WebSocketErrorModel)
class ResponseValidationError(ValidationException):
def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
super().__init__(errors)
self.body = body

1
fastapi/openapi/constants.py

@ -1,2 +1,3 @@
METHODS_WITH_BODY = {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"} METHODS_WITH_BODY = {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"}
REF_PREFIX = "#/components/schemas/" REF_PREFIX = "#/components/schemas/"
REF_TEMPLATE = "#/components/schemas/{model}"

216
fastapi/openapi/models.py

@ -1,12 +1,13 @@
from enum import Enum from enum import Enum
from typing import Any, Callable, Dict, Iterable, List, Optional, Union from typing import Any, Callable, Dict, Iterable, List, Optional, Union
from fastapi._compat import PYDANTIC_V2, _model_rebuild
from fastapi.logger import logger from fastapi.logger import logger
from pydantic import AnyUrl, BaseModel, Field from pydantic import AnyUrl, BaseModel, Field
from typing_extensions import Literal from typing_extensions import Literal
try: try:
import email_validator # type: ignore import email_validator
assert email_validator # make autoflake ignore the unused import assert email_validator # make autoflake ignore the unused import
from pydantic import EmailStr from pydantic import EmailStr
@ -31,16 +32,26 @@ class Contact(BaseModel):
url: Optional[AnyUrl] = None url: Optional[AnyUrl] = None
email: Optional[EmailStr] = None email: Optional[EmailStr] = None
class Config: if PYDANTIC_V2:
extra = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class License(BaseModel): class License(BaseModel):
name: str name: str
url: Optional[AnyUrl] = None url: Optional[AnyUrl] = None
class Config: if PYDANTIC_V2:
extra = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Info(BaseModel): class Info(BaseModel):
@ -51,8 +62,13 @@ class Info(BaseModel):
license: Optional[License] = None license: Optional[License] = None
version: str version: str
class Config: if PYDANTIC_V2:
extra = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class ServerVariable(BaseModel): class ServerVariable(BaseModel):
@ -60,8 +76,13 @@ class ServerVariable(BaseModel):
default: str default: str
description: Optional[str] = None description: Optional[str] = None
class Config: if PYDANTIC_V2:
extra = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Server(BaseModel): class Server(BaseModel):
@ -69,8 +90,13 @@ class Server(BaseModel):
description: Optional[str] = None description: Optional[str] = None
variables: Optional[Dict[str, ServerVariable]] = None variables: Optional[Dict[str, ServerVariable]] = None
class Config: if PYDANTIC_V2:
extra = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Reference(BaseModel): class Reference(BaseModel):
@ -89,16 +115,26 @@ class XML(BaseModel):
attribute: Optional[bool] = None attribute: Optional[bool] = None
wrapped: Optional[bool] = None wrapped: Optional[bool] = None
class Config: if PYDANTIC_V2:
extra = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class ExternalDocumentation(BaseModel): class ExternalDocumentation(BaseModel):
description: Optional[str] = None description: Optional[str] = None
url: AnyUrl url: AnyUrl
class Config: if PYDANTIC_V2:
extra = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Schema(BaseModel): class Schema(BaseModel):
@ -139,8 +175,13 @@ class Schema(BaseModel):
example: Optional[Any] = None example: Optional[Any] = None
deprecated: Optional[bool] = None deprecated: Optional[bool] = None
class Config: if PYDANTIC_V2:
extra: str = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Example(BaseModel): class Example(BaseModel):
@ -149,8 +190,13 @@ class Example(BaseModel):
value: Optional[Any] = None value: Optional[Any] = None
externalValue: Optional[AnyUrl] = None externalValue: Optional[AnyUrl] = None
class Config: if PYDANTIC_V2:
extra = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class ParameterInType(Enum): class ParameterInType(Enum):
@ -167,8 +213,13 @@ class Encoding(BaseModel):
explode: Optional[bool] = None explode: Optional[bool] = None
allowReserved: Optional[bool] = None allowReserved: Optional[bool] = None
class Config: if PYDANTIC_V2:
extra = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class MediaType(BaseModel): class MediaType(BaseModel):
@ -177,8 +228,13 @@ class MediaType(BaseModel):
examples: Optional[Dict[str, Union[Example, Reference]]] = None examples: Optional[Dict[str, Union[Example, Reference]]] = None
encoding: Optional[Dict[str, Encoding]] = None encoding: Optional[Dict[str, Encoding]] = None
class Config: if PYDANTIC_V2:
extra = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class ParameterBase(BaseModel): class ParameterBase(BaseModel):
@ -195,8 +251,13 @@ class ParameterBase(BaseModel):
# Serialization rules for more complex scenarios # Serialization rules for more complex scenarios
content: Optional[Dict[str, MediaType]] = None content: Optional[Dict[str, MediaType]] = None
class Config: if PYDANTIC_V2:
extra = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Parameter(ParameterBase): class Parameter(ParameterBase):
@ -213,8 +274,13 @@ class RequestBody(BaseModel):
content: Dict[str, MediaType] content: Dict[str, MediaType]
required: Optional[bool] = None required: Optional[bool] = None
class Config: if PYDANTIC_V2:
extra = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Link(BaseModel): class Link(BaseModel):
@ -225,8 +291,13 @@ class Link(BaseModel):
description: Optional[str] = None description: Optional[str] = None
server: Optional[Server] = None server: Optional[Server] = None
class Config: if PYDANTIC_V2:
extra = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Response(BaseModel): class Response(BaseModel):
@ -235,8 +306,13 @@ class Response(BaseModel):
content: Optional[Dict[str, MediaType]] = None content: Optional[Dict[str, MediaType]] = None
links: Optional[Dict[str, Union[Link, Reference]]] = None links: Optional[Dict[str, Union[Link, Reference]]] = None
class Config: if PYDANTIC_V2:
extra = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Operation(BaseModel): class Operation(BaseModel):
@ -254,8 +330,13 @@ class Operation(BaseModel):
security: Optional[List[Dict[str, List[str]]]] = None security: Optional[List[Dict[str, List[str]]]] = None
servers: Optional[List[Server]] = None servers: Optional[List[Server]] = None
class Config: if PYDANTIC_V2:
extra = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class PathItem(BaseModel): class PathItem(BaseModel):
@ -273,8 +354,13 @@ class PathItem(BaseModel):
servers: Optional[List[Server]] = None servers: Optional[List[Server]] = None
parameters: Optional[List[Union[Parameter, Reference]]] = None parameters: Optional[List[Union[Parameter, Reference]]] = None
class Config: if PYDANTIC_V2:
extra = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class SecuritySchemeType(Enum): class SecuritySchemeType(Enum):
@ -288,8 +374,13 @@ class SecurityBase(BaseModel):
type_: SecuritySchemeType = Field(alias="type") type_: SecuritySchemeType = Field(alias="type")
description: Optional[str] = None description: Optional[str] = None
class Config: if PYDANTIC_V2:
extra = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class APIKeyIn(Enum): class APIKeyIn(Enum):
@ -318,8 +409,13 @@ class OAuthFlow(BaseModel):
refreshUrl: Optional[str] = None refreshUrl: Optional[str] = None
scopes: Dict[str, str] = {} scopes: Dict[str, str] = {}
class Config: if PYDANTIC_V2:
extra = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class OAuthFlowImplicit(OAuthFlow): class OAuthFlowImplicit(OAuthFlow):
@ -345,8 +441,13 @@ class OAuthFlows(BaseModel):
clientCredentials: Optional[OAuthFlowClientCredentials] = None clientCredentials: Optional[OAuthFlowClientCredentials] = None
authorizationCode: Optional[OAuthFlowAuthorizationCode] = None authorizationCode: Optional[OAuthFlowAuthorizationCode] = None
class Config: if PYDANTIC_V2:
extra = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class OAuth2(SecurityBase): class OAuth2(SecurityBase):
@ -376,8 +477,13 @@ class Components(BaseModel):
# Using Any for Specification Extensions # Using Any for Specification Extensions
callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None
class Config: if PYDANTIC_V2:
extra = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Tag(BaseModel): class Tag(BaseModel):
@ -385,8 +491,13 @@ class Tag(BaseModel):
description: Optional[str] = None description: Optional[str] = None
externalDocs: Optional[ExternalDocumentation] = None externalDocs: Optional[ExternalDocumentation] = None
class Config: if PYDANTIC_V2:
extra = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class OpenAPI(BaseModel): class OpenAPI(BaseModel):
@ -400,10 +511,15 @@ class OpenAPI(BaseModel):
tags: Optional[List[Tag]] = None tags: Optional[List[Tag]] = None
externalDocs: Optional[ExternalDocumentation] = None externalDocs: Optional[ExternalDocumentation] = None
class Config: if PYDANTIC_V2:
extra = "allow" model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
Schema.update_forward_refs() _model_rebuild(Schema)
Operation.update_forward_refs() _model_rebuild(Operation)
Encoding.update_forward_refs() _model_rebuild(Encoding)

101
fastapi/openapi/utils.py

@ -1,32 +1,32 @@
import http.client import http.client
import inspect import inspect
import warnings import warnings
from enum import Enum
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast
from fastapi import routing from fastapi import routing
from fastapi._compat import (
GenerateJsonSchema,
ModelField,
Undefined,
get_compat_model_name_map,
get_definitions,
get_schema_from_model_field,
lenient_issubclass,
)
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 get_flat_dependant, get_flat_params from fastapi.dependencies.utils import get_flat_dependant, get_flat_params
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX, REF_TEMPLATE
from fastapi.openapi.models import OpenAPI from fastapi.openapi.models import OpenAPI
from fastapi.params import Body, Param from fastapi.params import Body, Param
from fastapi.responses import Response from fastapi.responses import Response
from fastapi.types import ModelNameMap
from fastapi.utils import ( from fastapi.utils import (
deep_dict_update, deep_dict_update,
generate_operation_id_for_path, generate_operation_id_for_path,
get_model_definitions,
is_body_allowed_for_status_code, is_body_allowed_for_status_code,
) )
from pydantic import BaseModel
from pydantic.fields import ModelField, Undefined
from pydantic.schema import (
field_schema,
get_flat_models_from_fields,
get_model_name_map,
)
from pydantic.utils import lenient_issubclass
from starlette.responses import JSONResponse from starlette.responses import JSONResponse
from starlette.routing import BaseRoute from starlette.routing import BaseRoute
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
@ -88,7 +88,8 @@ def get_openapi_security_definitions(
def get_openapi_operation_parameters( def get_openapi_operation_parameters(
*, *,
all_route_params: Sequence[ModelField], all_route_params: Sequence[ModelField],
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
parameters = [] parameters = []
for param in all_route_params: for param in all_route_params:
@ -96,13 +97,16 @@ def get_openapi_operation_parameters(
field_info = cast(Param, field_info) field_info = cast(Param, field_info)
if not field_info.include_in_schema: if not field_info.include_in_schema:
continue continue
param_schema = get_schema_from_model_field(
field=param,
schema_generator=schema_generator,
model_name_map=model_name_map,
)
parameter = { parameter = {
"name": param.alias, "name": param.alias,
"in": field_info.in_.value, "in": field_info.in_.value,
"required": param.required, "required": param.required,
"schema": field_schema( "schema": param_schema,
param, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)[0],
} }
if field_info.description: if field_info.description:
parameter["description"] = field_info.description parameter["description"] = field_info.description
@ -119,13 +123,16 @@ def get_openapi_operation_parameters(
def get_openapi_operation_request_body( def get_openapi_operation_request_body(
*, *,
body_field: Optional[ModelField], body_field: Optional[ModelField],
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
) -> Optional[Dict[str, Any]]: ) -> Optional[Dict[str, Any]]:
if not body_field: if not body_field:
return None return None
assert isinstance(body_field, ModelField) assert isinstance(body_field, ModelField)
body_schema, _, _ = field_schema( body_schema = get_schema_from_model_field(
body_field, model_name_map=model_name_map, ref_prefix=REF_PREFIX field=body_field,
schema_generator=schema_generator,
model_name_map=model_name_map,
) )
field_info = cast(Body, body_field.field_info) field_info = cast(Body, body_field.field_info)
request_media_type = field_info.media_type request_media_type = field_info.media_type
@ -190,7 +197,11 @@ def get_openapi_operation_metadata(
def get_openapi_path( def get_openapi_path(
*, route: routing.APIRoute, model_name_map: Dict[type, str], operation_ids: Set[str] *,
route: routing.APIRoute,
operation_ids: Set[str],
schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]: ) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
path = {} path = {}
security_schemes: Dict[str, Any] = {} security_schemes: Dict[str, Any] = {}
@ -218,7 +229,9 @@ def get_openapi_path(
security_schemes.update(security_definitions) security_schemes.update(security_definitions)
all_route_params = get_flat_params(route.dependant) all_route_params = get_flat_params(route.dependant)
operation_parameters = get_openapi_operation_parameters( operation_parameters = get_openapi_operation_parameters(
all_route_params=all_route_params, model_name_map=model_name_map all_route_params=all_route_params,
schema_generator=schema_generator,
model_name_map=model_name_map,
) )
parameters.extend(operation_parameters) parameters.extend(operation_parameters)
if parameters: if parameters:
@ -236,7 +249,9 @@ def get_openapi_path(
operation["parameters"] = list(all_parameters.values()) operation["parameters"] = list(all_parameters.values())
if method in METHODS_WITH_BODY: if method in METHODS_WITH_BODY:
request_body_oai = get_openapi_operation_request_body( request_body_oai = get_openapi_operation_request_body(
body_field=route.body_field, model_name_map=model_name_map body_field=route.body_field,
schema_generator=schema_generator,
model_name_map=model_name_map,
) )
if request_body_oai: if request_body_oai:
operation["requestBody"] = request_body_oai operation["requestBody"] = request_body_oai
@ -250,8 +265,9 @@ def get_openapi_path(
cb_definitions, cb_definitions,
) = get_openapi_path( ) = get_openapi_path(
route=callback, route=callback,
model_name_map=model_name_map,
operation_ids=operation_ids, operation_ids=operation_ids,
schema_generator=schema_generator,
model_name_map=model_name_map,
) )
callbacks[callback.name] = {callback.path: cb_path} callbacks[callback.name] = {callback.path: cb_path}
operation["callbacks"] = callbacks operation["callbacks"] = callbacks
@ -277,10 +293,10 @@ def get_openapi_path(
response_schema = {"type": "string"} response_schema = {"type": "string"}
if lenient_issubclass(current_response_class, JSONResponse): if lenient_issubclass(current_response_class, JSONResponse):
if route.response_field: if route.response_field:
response_schema, _, _ = field_schema( response_schema = get_schema_from_model_field(
route.response_field, field=route.response_field,
schema_generator=schema_generator,
model_name_map=model_name_map, model_name_map=model_name_map,
ref_prefix=REF_PREFIX,
) )
else: else:
response_schema = {} response_schema = {}
@ -309,8 +325,10 @@ def get_openapi_path(
field = route.response_fields.get(additional_status_code) field = route.response_fields.get(additional_status_code)
additional_field_schema: Optional[Dict[str, Any]] = None additional_field_schema: Optional[Dict[str, Any]] = None
if field: if field:
additional_field_schema, _, _ = field_schema( additional_field_schema = get_schema_from_model_field(
field, model_name_map=model_name_map, ref_prefix=REF_PREFIX field=field,
schema_generator=schema_generator,
model_name_map=model_name_map,
) )
media_type = route_response_media_type or "application/json" media_type = route_response_media_type or "application/json"
additional_schema = ( additional_schema = (
@ -356,13 +374,13 @@ def get_openapi_path(
return path, security_schemes, definitions return path, security_schemes, definitions
def get_flat_models_from_routes( def get_fields_from_routes(
routes: Sequence[BaseRoute], routes: Sequence[BaseRoute],
) -> Set[Union[Type[BaseModel], Type[Enum]]]: ) -> List[ModelField]:
body_fields_from_routes: List[ModelField] = [] body_fields_from_routes: List[ModelField] = []
responses_from_routes: List[ModelField] = [] responses_from_routes: List[ModelField] = []
request_fields_from_routes: List[ModelField] = [] request_fields_from_routes: List[ModelField] = []
callback_flat_models: Set[Union[Type[BaseModel], Type[Enum]]] = set() callback_flat_models: List[ModelField] = []
for route in routes: for route in routes:
if getattr(route, "include_in_schema", None) and isinstance( if getattr(route, "include_in_schema", None) and isinstance(
route, routing.APIRoute route, routing.APIRoute
@ -377,13 +395,12 @@ def get_flat_models_from_routes(
if route.response_fields: if route.response_fields:
responses_from_routes.extend(route.response_fields.values()) responses_from_routes.extend(route.response_fields.values())
if route.callbacks: if route.callbacks:
callback_flat_models |= get_flat_models_from_routes(route.callbacks) callback_flat_models.extend(get_fields_from_routes(route.callbacks))
params = get_flat_params(route.dependant) params = get_flat_params(route.dependant)
request_fields_from_routes.extend(params) request_fields_from_routes.extend(params)
flat_models = callback_flat_models | get_flat_models_from_fields( flat_models = callback_flat_models + list(
body_fields_from_routes + responses_from_routes + request_fields_from_routes, body_fields_from_routes + responses_from_routes + request_fields_from_routes
known_models=set(),
) )
return flat_models return flat_models
@ -416,15 +433,21 @@ def get_openapi(
components: Dict[str, Dict[str, Any]] = {} components: Dict[str, Dict[str, Any]] = {}
paths: Dict[str, Dict[str, Any]] = {} paths: Dict[str, Dict[str, Any]] = {}
operation_ids: Set[str] = set() operation_ids: Set[str] = set()
flat_models = get_flat_models_from_routes(routes) all_fields = get_fields_from_routes(routes)
model_name_map = get_model_name_map(flat_models) model_name_map = get_compat_model_name_map(all_fields)
definitions = get_model_definitions( schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
flat_models=flat_models, model_name_map=model_name_map definitions = get_definitions(
fields=all_fields,
schema_generator=schema_generator,
model_name_map=model_name_map,
) )
for route in routes: for route in routes:
if isinstance(route, routing.APIRoute): if isinstance(route, routing.APIRoute):
result = get_openapi_path( result = get_openapi_path(
route=route, model_name_map=model_name_map, operation_ids=operation_ids route=route,
operation_ids=operation_ids,
schema_generator=schema_generator,
model_name_map=model_name_map,
) )
if result: if result:
path, security_schemes, path_definitions = result path, security_schemes, path_definitions = result

16
fastapi/param_functions.py

@ -1,7 +1,7 @@
from typing import Any, Callable, Dict, Optional, Sequence from typing import Any, Callable, Dict, Optional, Sequence
from fastapi import params from fastapi import params
from pydantic.fields import Undefined from fastapi._compat import Undefined
def Path( # noqa: N802 def Path( # noqa: N802
@ -16,6 +16,7 @@ def Path( # noqa: N802
le: Optional[float] = None, le: Optional[float] = None,
min_length: Optional[int] = None, min_length: Optional[int] = None,
max_length: Optional[int] = None, max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None, regex: Optional[str] = None,
example: Any = Undefined, example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None, examples: Optional[Dict[str, Any]] = None,
@ -34,6 +35,7 @@ def Path( # noqa: N802
le=le, le=le,
min_length=min_length, min_length=min_length,
max_length=max_length, max_length=max_length,
pattern=pattern,
regex=regex, regex=regex,
example=example, example=example,
examples=examples, examples=examples,
@ -55,6 +57,7 @@ def Query( # noqa: N802
le: Optional[float] = None, le: Optional[float] = None,
min_length: Optional[int] = None, min_length: Optional[int] = None,
max_length: Optional[int] = None, max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None, regex: Optional[str] = None,
example: Any = Undefined, example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None, examples: Optional[Dict[str, Any]] = None,
@ -73,6 +76,7 @@ def Query( # noqa: N802
le=le, le=le,
min_length=min_length, min_length=min_length,
max_length=max_length, max_length=max_length,
pattern=pattern,
regex=regex, regex=regex,
example=example, example=example,
examples=examples, examples=examples,
@ -95,6 +99,7 @@ def Header( # noqa: N802
le: Optional[float] = None, le: Optional[float] = None,
min_length: Optional[int] = None, min_length: Optional[int] = None,
max_length: Optional[int] = None, max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None, regex: Optional[str] = None,
example: Any = Undefined, example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None, examples: Optional[Dict[str, Any]] = None,
@ -114,6 +119,7 @@ def Header( # noqa: N802
le=le, le=le,
min_length=min_length, min_length=min_length,
max_length=max_length, max_length=max_length,
pattern=pattern,
regex=regex, regex=regex,
example=example, example=example,
examples=examples, examples=examples,
@ -135,6 +141,7 @@ def Cookie( # noqa: N802
le: Optional[float] = None, le: Optional[float] = None,
min_length: Optional[int] = None, min_length: Optional[int] = None,
max_length: Optional[int] = None, max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None, regex: Optional[str] = None,
example: Any = Undefined, example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None, examples: Optional[Dict[str, Any]] = None,
@ -153,6 +160,7 @@ def Cookie( # noqa: N802
le=le, le=le,
min_length=min_length, min_length=min_length,
max_length=max_length, max_length=max_length,
pattern=pattern,
regex=regex, regex=regex,
example=example, example=example,
examples=examples, examples=examples,
@ -176,6 +184,7 @@ def Body( # noqa: N802
le: Optional[float] = None, le: Optional[float] = None,
min_length: Optional[int] = None, min_length: Optional[int] = None,
max_length: Optional[int] = None, max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None, regex: Optional[str] = None,
example: Any = Undefined, example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None, examples: Optional[Dict[str, Any]] = None,
@ -194,6 +203,7 @@ def Body( # noqa: N802
le=le, le=le,
min_length=min_length, min_length=min_length,
max_length=max_length, max_length=max_length,
pattern=pattern,
regex=regex, regex=regex,
example=example, example=example,
examples=examples, examples=examples,
@ -214,6 +224,7 @@ def Form( # noqa: N802
le: Optional[float] = None, le: Optional[float] = None,
min_length: Optional[int] = None, min_length: Optional[int] = None,
max_length: Optional[int] = None, max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None, regex: Optional[str] = None,
example: Any = Undefined, example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None, examples: Optional[Dict[str, Any]] = None,
@ -231,6 +242,7 @@ def Form( # noqa: N802
le=le, le=le,
min_length=min_length, min_length=min_length,
max_length=max_length, max_length=max_length,
pattern=pattern,
regex=regex, regex=regex,
example=example, example=example,
examples=examples, examples=examples,
@ -251,6 +263,7 @@ def File( # noqa: N802
le: Optional[float] = None, le: Optional[float] = None,
min_length: Optional[int] = None, min_length: Optional[int] = None,
max_length: Optional[int] = None, max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None, regex: Optional[str] = None,
example: Any = Undefined, example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None, examples: Optional[Dict[str, Any]] = None,
@ -268,6 +281,7 @@ def File( # noqa: N802
le=le, le=le,
min_length=min_length, min_length=min_length,
max_length=max_length, max_length=max_length,
pattern=pattern,
regex=regex, regex=regex,
example=example, example=example,
examples=examples, examples=examples,

65
fastapi/params.py

@ -1,7 +1,9 @@
from enum import Enum from enum import Enum
from typing import Any, Callable, Dict, Optional, Sequence from typing import Any, Callable, Dict, Optional, Sequence, Type
from pydantic.fields import FieldInfo, Undefined from pydantic.fields import FieldInfo
from ._compat import PYDANTIC_V2, Undefined
class ParamTypes(Enum): class ParamTypes(Enum):
@ -18,6 +20,7 @@ class Param(FieldInfo):
self, self,
default: Any = Undefined, default: Any = Undefined,
*, *,
annotation: Optional[Type[Any]] = None,
alias: Optional[str] = None, alias: Optional[str] = None,
title: Optional[str] = None, title: Optional[str] = None,
description: Optional[str] = None, description: Optional[str] = None,
@ -27,6 +30,7 @@ class Param(FieldInfo):
le: Optional[float] = None, le: Optional[float] = None,
min_length: Optional[int] = None, min_length: Optional[int] = None,
max_length: Optional[int] = None, max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None, regex: Optional[str] = None,
example: Any = Undefined, example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None, examples: Optional[Dict[str, Any]] = None,
@ -36,9 +40,8 @@ class Param(FieldInfo):
): ):
self.deprecated = deprecated self.deprecated = deprecated
self.example = example self.example = example
self.examples = examples
self.include_in_schema = include_in_schema self.include_in_schema = include_in_schema
super().__init__( kwargs = dict(
default=default, default=default,
alias=alias, alias=alias,
title=title, title=title,
@ -49,9 +52,19 @@ class Param(FieldInfo):
le=le, le=le,
min_length=min_length, min_length=min_length,
max_length=max_length, max_length=max_length,
regex=regex,
**extra, **extra,
) )
if PYDANTIC_V2:
kwargs["annotation"] = annotation
kwargs["pattern"] = pattern or regex
else:
# TODO: pv2 figure out how to deprecate regex
kwargs["regex"] = pattern or regex
super().__init__(**kwargs)
# TODO: pv2 decide how to handle OpenAPI examples vs JSON Schema examples
# and how to deprecate OpenAPI examples
self.examples = examples # type: ignore[assignment]
def __repr__(self) -> str: def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.default})" return f"{self.__class__.__name__}({self.default})"
@ -64,6 +77,7 @@ class Path(Param):
self, self,
default: Any = ..., default: Any = ...,
*, *,
annotation: Optional[Type[Any]] = None,
alias: Optional[str] = None, alias: Optional[str] = None,
title: Optional[str] = None, title: Optional[str] = None,
description: Optional[str] = None, description: Optional[str] = None,
@ -73,6 +87,7 @@ class Path(Param):
le: Optional[float] = None, le: Optional[float] = None,
min_length: Optional[int] = None, min_length: Optional[int] = None,
max_length: Optional[int] = None, max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None, regex: Optional[str] = None,
example: Any = Undefined, example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None, examples: Optional[Dict[str, Any]] = None,
@ -84,6 +99,7 @@ class Path(Param):
self.in_ = self.in_ self.in_ = self.in_
super().__init__( super().__init__(
default=default, default=default,
annotation=annotation,
alias=alias, alias=alias,
title=title, title=title,
description=description, description=description,
@ -93,6 +109,7 @@ class Path(Param):
le=le, le=le,
min_length=min_length, min_length=min_length,
max_length=max_length, max_length=max_length,
pattern=pattern,
regex=regex, regex=regex,
deprecated=deprecated, deprecated=deprecated,
example=example, example=example,
@ -109,6 +126,7 @@ class Query(Param):
self, self,
default: Any = Undefined, default: Any = Undefined,
*, *,
annotation: Optional[Type[Any]] = None,
alias: Optional[str] = None, alias: Optional[str] = None,
title: Optional[str] = None, title: Optional[str] = None,
description: Optional[str] = None, description: Optional[str] = None,
@ -118,6 +136,7 @@ class Query(Param):
le: Optional[float] = None, le: Optional[float] = None,
min_length: Optional[int] = None, min_length: Optional[int] = None,
max_length: Optional[int] = None, max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None, regex: Optional[str] = None,
example: Any = Undefined, example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None, examples: Optional[Dict[str, Any]] = None,
@ -127,6 +146,7 @@ class Query(Param):
): ):
super().__init__( super().__init__(
default=default, default=default,
annotation=annotation,
alias=alias, alias=alias,
title=title, title=title,
description=description, description=description,
@ -136,6 +156,7 @@ class Query(Param):
le=le, le=le,
min_length=min_length, min_length=min_length,
max_length=max_length, max_length=max_length,
pattern=pattern,
regex=regex, regex=regex,
deprecated=deprecated, deprecated=deprecated,
example=example, example=example,
@ -152,6 +173,7 @@ class Header(Param):
self, self,
default: Any = Undefined, default: Any = Undefined,
*, *,
annotation: Optional[Type[Any]] = None,
alias: Optional[str] = None, alias: Optional[str] = None,
convert_underscores: bool = True, convert_underscores: bool = True,
title: Optional[str] = None, title: Optional[str] = None,
@ -162,6 +184,7 @@ class Header(Param):
le: Optional[float] = None, le: Optional[float] = None,
min_length: Optional[int] = None, min_length: Optional[int] = None,
max_length: Optional[int] = None, max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None, regex: Optional[str] = None,
example: Any = Undefined, example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None, examples: Optional[Dict[str, Any]] = None,
@ -172,6 +195,7 @@ class Header(Param):
self.convert_underscores = convert_underscores self.convert_underscores = convert_underscores
super().__init__( super().__init__(
default=default, default=default,
annotation=annotation,
alias=alias, alias=alias,
title=title, title=title,
description=description, description=description,
@ -181,6 +205,7 @@ class Header(Param):
le=le, le=le,
min_length=min_length, min_length=min_length,
max_length=max_length, max_length=max_length,
pattern=pattern,
regex=regex, regex=regex,
deprecated=deprecated, deprecated=deprecated,
example=example, example=example,
@ -197,6 +222,7 @@ class Cookie(Param):
self, self,
default: Any = Undefined, default: Any = Undefined,
*, *,
annotation: Optional[Type[Any]] = None,
alias: Optional[str] = None, alias: Optional[str] = None,
title: Optional[str] = None, title: Optional[str] = None,
description: Optional[str] = None, description: Optional[str] = None,
@ -206,6 +232,7 @@ class Cookie(Param):
le: Optional[float] = None, le: Optional[float] = None,
min_length: Optional[int] = None, min_length: Optional[int] = None,
max_length: Optional[int] = None, max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None, regex: Optional[str] = None,
example: Any = Undefined, example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None, examples: Optional[Dict[str, Any]] = None,
@ -215,6 +242,7 @@ class Cookie(Param):
): ):
super().__init__( super().__init__(
default=default, default=default,
annotation=annotation,
alias=alias, alias=alias,
title=title, title=title,
description=description, description=description,
@ -224,6 +252,7 @@ class Cookie(Param):
le=le, le=le,
min_length=min_length, min_length=min_length,
max_length=max_length, max_length=max_length,
pattern=pattern,
regex=regex, regex=regex,
deprecated=deprecated, deprecated=deprecated,
example=example, example=example,
@ -238,6 +267,7 @@ class Body(FieldInfo):
self, self,
default: Any = Undefined, default: Any = Undefined,
*, *,
annotation: Optional[Type[Any]] = None,
embed: bool = False, embed: bool = False,
media_type: str = "application/json", media_type: str = "application/json",
alias: Optional[str] = None, alias: Optional[str] = None,
@ -249,6 +279,7 @@ class Body(FieldInfo):
le: Optional[float] = None, le: Optional[float] = None,
min_length: Optional[int] = None, min_length: Optional[int] = None,
max_length: Optional[int] = None, max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None, regex: Optional[str] = None,
example: Any = Undefined, example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None, examples: Optional[Dict[str, Any]] = None,
@ -257,8 +288,7 @@ class Body(FieldInfo):
self.embed = embed self.embed = embed
self.media_type = media_type self.media_type = media_type
self.example = example self.example = example
self.examples = examples kwargs = dict(
super().__init__(
default=default, default=default,
alias=alias, alias=alias,
title=title, title=title,
@ -269,9 +299,20 @@ class Body(FieldInfo):
le=le, le=le,
min_length=min_length, min_length=min_length,
max_length=max_length, max_length=max_length,
regex=regex,
**extra, **extra,
) )
if PYDANTIC_V2:
kwargs["annotation"] = annotation
kwargs["pattern"] = pattern or regex
else:
# TODO: pv2 figure out how to deprecate regex
kwargs["regex"] = pattern or regex
super().__init__(
**kwargs,
)
# TODO: pv2 decide how to handle OpenAPI examples vs JSON Schema examples
# and how to deprecate OpenAPI examples
self.examples = examples # type: ignore[assignment]
def __repr__(self) -> str: def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.default})" return f"{self.__class__.__name__}({self.default})"
@ -282,6 +323,7 @@ class Form(Body):
self, self,
default: Any = Undefined, default: Any = Undefined,
*, *,
annotation: Optional[Type[Any]] = None,
media_type: str = "application/x-www-form-urlencoded", media_type: str = "application/x-www-form-urlencoded",
alias: Optional[str] = None, alias: Optional[str] = None,
title: Optional[str] = None, title: Optional[str] = None,
@ -292,6 +334,7 @@ class Form(Body):
le: Optional[float] = None, le: Optional[float] = None,
min_length: Optional[int] = None, min_length: Optional[int] = None,
max_length: Optional[int] = None, max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None, regex: Optional[str] = None,
example: Any = Undefined, example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None, examples: Optional[Dict[str, Any]] = None,
@ -299,6 +342,7 @@ class Form(Body):
): ):
super().__init__( super().__init__(
default=default, default=default,
annotation=annotation,
embed=True, embed=True,
media_type=media_type, media_type=media_type,
alias=alias, alias=alias,
@ -310,6 +354,7 @@ class Form(Body):
le=le, le=le,
min_length=min_length, min_length=min_length,
max_length=max_length, max_length=max_length,
pattern=pattern,
regex=regex, regex=regex,
example=example, example=example,
examples=examples, examples=examples,
@ -322,6 +367,7 @@ class File(Form):
self, self,
default: Any = Undefined, default: Any = Undefined,
*, *,
annotation: Optional[Type[Any]] = None,
media_type: str = "multipart/form-data", media_type: str = "multipart/form-data",
alias: Optional[str] = None, alias: Optional[str] = None,
title: Optional[str] = None, title: Optional[str] = None,
@ -332,6 +378,7 @@ class File(Form):
le: Optional[float] = None, le: Optional[float] = None,
min_length: Optional[int] = None, min_length: Optional[int] = None,
max_length: Optional[int] = None, max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None, regex: Optional[str] = None,
example: Any = Undefined, example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None, examples: Optional[Dict[str, Any]] = None,
@ -339,6 +386,7 @@ class File(Form):
): ):
super().__init__( super().__init__(
default=default, default=default,
annotation=annotation,
media_type=media_type, media_type=media_type,
alias=alias, alias=alias,
title=title, title=title,
@ -349,6 +397,7 @@ class File(Form):
le=le, le=le,
min_length=min_length, min_length=min_length,
max_length=max_length, max_length=max_length,
pattern=pattern,
regex=regex, regex=regex,
example=example, example=example,
examples=examples, examples=examples,

125
fastapi/routing.py

@ -20,6 +20,14 @@ from typing import (
) )
from fastapi import params from fastapi import params
from fastapi._compat import (
ModelField,
Undefined,
_get_model_config,
_model_dump,
_normalize_errors,
lenient_issubclass,
)
from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.datastructures import Default, DefaultPlaceholder
from fastapi.dependencies.models import Dependant from fastapi.dependencies.models import Dependant
from fastapi.dependencies.utils import ( from fastapi.dependencies.utils import (
@ -29,13 +37,14 @@ from fastapi.dependencies.utils import (
get_typed_return_annotation, get_typed_return_annotation,
solve_dependencies, solve_dependencies,
) )
from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import ( from fastapi.exceptions import (
FastAPIError, FastAPIError,
RequestValidationError, RequestValidationError,
ResponseValidationError,
WebSocketRequestValidationError, WebSocketRequestValidationError,
) )
from fastapi.types import DecoratedCallable from fastapi.types import DecoratedCallable, IncEx
from fastapi.utils import ( from fastapi.utils import (
create_cloned_field, create_cloned_field,
create_response_field, create_response_field,
@ -44,9 +53,6 @@ from fastapi.utils import (
is_body_allowed_for_status_code, is_body_allowed_for_status_code,
) )
from pydantic import BaseModel from pydantic import BaseModel
from pydantic.error_wrappers import ErrorWrapper, ValidationError
from pydantic.fields import ModelField, Undefined
from pydantic.utils import lenient_issubclass
from starlette import routing from starlette import routing
from starlette.concurrency import run_in_threadpool from starlette.concurrency import run_in_threadpool
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
@ -73,14 +79,15 @@ def _prepare_response_content(
exclude_none: bool = False, exclude_none: bool = False,
) -> Any: ) -> Any:
if isinstance(res, BaseModel): if isinstance(res, BaseModel):
read_with_orm_mode = getattr(res.__config__, "read_with_orm_mode", None) read_with_orm_mode = getattr(_get_model_config(res), "read_with_orm_mode", None)
if read_with_orm_mode: if read_with_orm_mode:
# Let from_orm extract the data from this model instead of converting # Let from_orm extract the data from this model instead of converting
# it now to a dict. # it now to a dict.
# Otherwise there's no way to extract lazy data that requires attribute # Otherwise there's no way to extract lazy data that requires attribute
# access instead of dict iteration, e.g. lazy relationships. # access instead of dict iteration, e.g. lazy relationships.
return res return res
return res.dict( return _model_dump(
res,
by_alias=True, by_alias=True,
exclude_unset=exclude_unset, exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults, exclude_defaults=exclude_defaults,
@ -115,8 +122,8 @@ async def serialize_response(
*, *,
field: Optional[ModelField] = None, field: Optional[ModelField] = None,
response_content: Any, response_content: Any,
include: Optional[Union[SetIntStr, DictIntStrAny]] = None, include: Optional[IncEx] = None,
exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, exclude: Optional[IncEx] = None,
by_alias: bool = True, by_alias: bool = True,
exclude_unset: bool = False, exclude_unset: bool = False,
exclude_defaults: bool = False, exclude_defaults: bool = False,
@ -125,24 +132,40 @@ async def serialize_response(
) -> Any: ) -> Any:
if field: if field:
errors = [] errors = []
response_content = _prepare_response_content( if not hasattr(field, "serialize"):
response_content, # pydantic v1
exclude_unset=exclude_unset, response_content = _prepare_response_content(
exclude_defaults=exclude_defaults, response_content,
exclude_none=exclude_none, exclude_unset=exclude_unset,
) exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
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_, ErrorWrapper): if isinstance(errors_, list):
errors.append(errors_)
elif isinstance(errors_, list):
errors.extend(errors_) errors.extend(errors_)
elif errors_:
errors.append(errors_)
if errors: if errors:
raise ValidationError(errors, field.type_) raise ResponseValidationError(
errors=_normalize_errors(errors), body=response_content
)
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 jsonable_encoder(
value, value,
include=include, include=include,
@ -175,8 +198,8 @@ def get_request_handler(
status_code: Optional[int] = None, status_code: Optional[int] = None,
response_class: Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse), response_class: Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse),
response_field: Optional[ModelField] = None, response_field: Optional[ModelField] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False, response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,
@ -220,7 +243,16 @@ def get_request_handler(
body = body_bytes body = body_bytes
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
raise RequestValidationError( raise RequestValidationError(
[ErrorWrapper(e, ("body", e.pos))], body=e.doc [
{
"type": "json_invalid",
"loc": ("body", e.pos),
"msg": "JSON decode error",
"input": {},
"ctx": {"error": e.msg},
}
],
body=e.doc,
) from e ) from e
except HTTPException: except HTTPException:
raise raise
@ -236,7 +268,7 @@ def get_request_handler(
) )
values, errors, background_tasks, sub_response, _ = solved_result values, errors, background_tasks, sub_response, _ = solved_result
if errors: if errors:
raise RequestValidationError(errors, body=body) raise RequestValidationError(_normalize_errors(errors), body=body)
else: else:
raw_response = await run_endpoint_function( raw_response = await run_endpoint_function(
dependant=dependant, values=values, is_coroutine=is_coroutine dependant=dependant, values=values, is_coroutine=is_coroutine
@ -287,7 +319,7 @@ def get_websocket_app(
) )
values, errors, _, _2, _3 = solved_result values, errors, _, _2, _3 = solved_result
if errors: if errors:
raise WebSocketRequestValidationError(errors) raise WebSocketRequestValidationError(_normalize_errors(errors))
assert dependant.call is not None, "dependant.call must be a function" assert dependant.call is not None, "dependant.call must be a function"
await dependant.call(**values) await dependant.call(**values)
@ -348,8 +380,8 @@ class APIRoute(routing.Route):
name: Optional[str] = None, name: Optional[str] = None,
methods: Optional[Union[Set[str], List[str]]] = None, methods: Optional[Union[Set[str], List[str]]] = None,
operation_id: Optional[str] = None, operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False, response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,
@ -423,6 +455,7 @@ class APIRoute(routing.Route):
# would pass the validation and be returned as is. # would pass the validation and be returned as is.
# By being a new field, no inheritance will be passed as is. A new model # By being a new field, no inheritance will be passed as is. A new model
# will be always created. # will be always created.
# TODO: remove when deprecating Pydantic v1
self.secure_cloned_response_field: Optional[ self.secure_cloned_response_field: Optional[
ModelField ModelField
] = create_cloned_field(self.response_field) ] = create_cloned_field(self.response_field)
@ -569,8 +602,8 @@ class APIRouter(routing.Router):
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
methods: Optional[Union[Set[str], List[str]]] = None, methods: Optional[Union[Set[str], List[str]]] = None,
operation_id: Optional[str] = None, operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False, response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,
@ -650,8 +683,8 @@ class APIRouter(routing.Router):
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
methods: Optional[List[str]] = None, methods: Optional[List[str]] = None,
operation_id: Optional[str] = None, operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False, response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,
@ -877,8 +910,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
operation_id: Optional[str] = None, operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False, response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,
@ -933,8 +966,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
operation_id: Optional[str] = None, operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False, response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,
@ -989,8 +1022,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
operation_id: Optional[str] = None, operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False, response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,
@ -1045,8 +1078,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
operation_id: Optional[str] = None, operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False, response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,
@ -1101,8 +1134,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
operation_id: Optional[str] = None, operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False, response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,
@ -1157,8 +1190,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
operation_id: Optional[str] = None, operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False, response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,
@ -1213,8 +1246,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
operation_id: Optional[str] = None, operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False, response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,
@ -1269,8 +1302,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
operation_id: Optional[str] = None, operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False, response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,

31
fastapi/security/oauth2.py

@ -1,5 +1,11 @@
import sys
from typing import Any, Dict, List, Optional, Union, cast from typing import Any, Dict, List, Optional, Union, cast
if sys.version_info < (3, 9):
from typing_extensions import Annotated
else:
from typing import Annotated
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from fastapi.openapi.models import OAuth2 as OAuth2Model from fastapi.openapi.models import OAuth2 as OAuth2Model
from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
@ -45,12 +51,13 @@ class OAuth2PasswordRequestForm:
def __init__( def __init__(
self, self,
grant_type: str = Form(default=None, regex="password"), *,
username: str = Form(), grant_type: Annotated[Union[str, None], Form(pattern="password")] = None,
password: str = Form(), username: Annotated[str, Form()],
scope: str = Form(default=""), password: Annotated[str, Form()],
client_id: Optional[str] = Form(default=None), scope: Annotated[str, Form()] = "",
client_secret: Optional[str] = Form(default=None), client_id: Annotated[Union[str, None], Form()] = None,
client_secret: Annotated[Union[str, None], Form()] = None,
): ):
self.grant_type = grant_type self.grant_type = grant_type
self.username = username self.username = username
@ -95,12 +102,12 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm):
def __init__( def __init__(
self, self,
grant_type: str = Form(regex="password"), grant_type: Annotated[str, Form(pattern="password")],
username: str = Form(), username: Annotated[str, Form()],
password: str = Form(), password: Annotated[str, Form()],
scope: str = Form(default=""), scope: Annotated[str, Form()] = "",
client_id: Optional[str] = Form(default=None), client_id: Annotated[Union[str, None], Form()] = None,
client_secret: Optional[str] = Form(default=None), client_secret: Annotated[Union[str, None], Form()] = None,
): ):
super().__init__( super().__init__(
grant_type=grant_type, grant_type=grant_type,

10
fastapi/types.py

@ -1,3 +1,11 @@
from typing import Any, Callable, TypeVar import types
from enum import Enum
from typing import Any, Callable, Dict, Set, Type, TypeVar, Union
from pydantic import BaseModel
DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any]) DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any])
UnionType = getattr(types, "UnionType", Union)
NoneType = getattr(types, "UnionType", None)
ModelNameMap = Dict[Union[Type[BaseModel], Type[Enum]], str]
IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]]

121
fastapi/utils.py

@ -1,7 +1,6 @@
import re import re
import warnings import warnings
from dataclasses import is_dataclass from dataclasses import is_dataclass
from enum import Enum
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
@ -16,13 +15,20 @@ from typing import (
from weakref import WeakKeyDictionary from weakref import WeakKeyDictionary
import fastapi import fastapi
from dirty_equals import IsStr
from fastapi._compat import (
PYDANTIC_V2,
BaseConfig,
ModelField,
PydanticSchemaGenerationError,
Undefined,
UndefinedType,
Validator,
lenient_issubclass,
)
from fastapi.datastructures import DefaultPlaceholder, DefaultType from fastapi.datastructures import DefaultPlaceholder, DefaultType
from fastapi.openapi.constants import REF_PREFIX from pydantic import BaseModel, create_model
from pydantic import BaseConfig, BaseModel, create_model from pydantic.fields import FieldInfo
from pydantic.class_validators import Validator
from pydantic.fields import FieldInfo, ModelField, UndefinedType
from pydantic.schema import model_process_schema
from pydantic.utils import lenient_issubclass
if TYPE_CHECKING: # pragma: nocover if TYPE_CHECKING: # pragma: nocover
from .routing import APIRoute from .routing import APIRoute
@ -50,24 +56,6 @@ def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool:
return not (current_status_code < 200 or current_status_code in {204, 304}) return not (current_status_code < 200 or current_status_code in {204, 304})
def get_model_definitions(
*,
flat_models: Set[Union[Type[BaseModel], Type[Enum]]],
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
) -> Dict[str, Any]:
definitions: Dict[str, Dict[str, Any]] = {}
for model in flat_models:
m_schema, m_definitions, m_nested_models = model_process_schema(
model, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)
definitions.update(m_definitions)
model_name = model_name_map[model]
if "description" in m_schema:
m_schema["description"] = m_schema["description"].split("\f")[0]
definitions[model_name] = m_schema
return definitions
def get_path_param_names(path: str) -> Set[str]: def get_path_param_names(path: str) -> Set[str]:
return set(re.findall("{(.*?)}", path)) return set(re.findall("{(.*?)}", path))
@ -76,8 +64,8 @@ def create_response_field(
name: str, name: str,
type_: Type[Any], type_: Type[Any],
class_validators: Optional[Dict[str, Validator]] = None, class_validators: Optional[Dict[str, Validator]] = None,
default: Optional[Any] = None, default: Optional[Any] = Undefined,
required: Union[bool, UndefinedType] = True, required: Union[bool, UndefinedType] = Undefined,
model_config: Type[BaseConfig] = BaseConfig, model_config: Type[BaseConfig] = BaseConfig,
field_info: Optional[FieldInfo] = None, field_info: Optional[FieldInfo] = None,
alias: Optional[str] = None, alias: Optional[str] = None,
@ -86,20 +74,27 @@ def create_response_field(
Create a new response field. Raises if type_ is invalid. Create a new response field. Raises if type_ is invalid.
""" """
class_validators = class_validators or {} class_validators = class_validators or {}
field_info = field_info or FieldInfo() if PYDANTIC_V2:
field_info = field_info or FieldInfo(
try: annotation=type_, default=default, alias=alias
return ModelField( )
name=name, else:
type_=type_, field_info = field_info or FieldInfo()
class_validators=class_validators, kwargs = {"name": name, "field_info": field_info}
default=default, if not PYDANTIC_V2:
required=required, kwargs.update(
model_config=model_config, {
alias=alias, "type_": type_,
field_info=field_info, "class_validators": class_validators,
"default": default,
"required": required,
"model_config": model_config,
"alias": alias,
}
) )
except RuntimeError: try:
return ModelField(**kwargs) # type: ignore[arg-type]
except (RuntimeError, PydanticSchemaGenerationError):
raise fastapi.exceptions.FastAPIError( raise fastapi.exceptions.FastAPIError(
"Invalid args for response field! Hint: " "Invalid args for response field! Hint: "
f"check that {type_} is a valid Pydantic field type. " f"check that {type_} is a valid Pydantic field type. "
@ -116,6 +111,8 @@ def create_cloned_field(
*, *,
cloned_types: Optional[MutableMapping[Type[BaseModel], Type[BaseModel]]] = None, cloned_types: Optional[MutableMapping[Type[BaseModel], Type[BaseModel]]] = None,
) -> ModelField: ) -> ModelField:
if PYDANTIC_V2:
return field
# cloned_types caches already cloned types to support recursive models and improve # cloned_types caches already cloned types to support recursive models and improve
# performance by avoiding unecessary cloning # performance by avoiding unecessary cloning
if cloned_types is None: if cloned_types is None:
@ -136,30 +133,30 @@ def create_cloned_field(
f, cloned_types=cloned_types f, cloned_types=cloned_types
) )
new_field = create_response_field(name=field.name, type_=use_type) new_field = create_response_field(name=field.name, type_=use_type)
new_field.has_alias = field.has_alias new_field.has_alias = field.has_alias # type: ignore[attr-defined]
new_field.alias = field.alias new_field.alias = field.alias # type: ignore[misc]
new_field.class_validators = field.class_validators new_field.class_validators = field.class_validators # type: ignore[attr-defined]
new_field.default = field.default new_field.default = field.default # type: ignore[misc]
new_field.required = field.required new_field.required = field.required # type: ignore[misc]
new_field.model_config = field.model_config new_field.model_config = field.model_config # type: ignore[attr-defined]
new_field.field_info = field.field_info new_field.field_info = field.field_info
new_field.allow_none = field.allow_none new_field.allow_none = field.allow_none # type: ignore[attr-defined]
new_field.validate_always = field.validate_always new_field.validate_always = field.validate_always # type: ignore[attr-defined]
if field.sub_fields: if field.sub_fields: # type: ignore[attr-defined]
new_field.sub_fields = [ new_field.sub_fields = [ # type: ignore[attr-defined]
create_cloned_field(sub_field, cloned_types=cloned_types) create_cloned_field(sub_field, cloned_types=cloned_types)
for sub_field in field.sub_fields for sub_field in field.sub_fields # type: ignore[attr-defined]
] ]
if field.key_field: if field.key_field: # type: ignore[attr-defined]
new_field.key_field = create_cloned_field( new_field.key_field = create_cloned_field( # type: ignore[attr-defined]
field.key_field, cloned_types=cloned_types field.key_field, cloned_types=cloned_types # type: ignore[attr-defined]
) )
new_field.validators = field.validators new_field.validators = field.validators # type: ignore[attr-defined]
new_field.pre_validators = field.pre_validators new_field.pre_validators = field.pre_validators # type: ignore[attr-defined]
new_field.post_validators = field.post_validators new_field.post_validators = field.post_validators # type: ignore[attr-defined]
new_field.parse_json = field.parse_json new_field.parse_json = field.parse_json # type: ignore[attr-defined]
new_field.shape = field.shape new_field.shape = field.shape # type: ignore[attr-defined]
new_field.populate_validators() new_field.populate_validators() # type: ignore[attr-defined]
return new_field return new_field
@ -220,3 +217,7 @@ def get_value_or_default(
if not isinstance(item, DefaultPlaceholder): if not isinstance(item, DefaultPlaceholder):
return item return item
return first_item return first_item
def match_pydantic_error_url(error_type: str) -> IsStr:
return IsStr(regex=rf"^https://errors\.pydantic\.dev/.*/v/{error_type}")

5
pyproject.toml

@ -42,7 +42,7 @@ classifiers = [
] ]
dependencies = [ dependencies = [
"starlette>=0.27.0,<0.28.0", "starlette>=0.27.0,<0.28.0",
"pydantic>=1.7.4,!=1.8,!=1.8.1,<2.0.0", "pydantic>=1.7.4,!=1.8,!=1.8.1,<3.0.0",
] ]
dynamic = ["version"] dynamic = ["version"]
@ -59,7 +59,7 @@ all = [
"pyyaml >=5.3.1", "pyyaml >=5.3.1",
"ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0", "ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0",
"orjson >=3.2.1", "orjson >=3.2.1",
"email_validator >=1.1.1", "email_validator >=2.0.0",
"uvicorn[standard] >=0.12.0", "uvicorn[standard] >=0.12.0",
] ]
@ -83,6 +83,7 @@ check_untyped_defs = true
addopts = [ addopts = [
"--strict-config", "--strict-config",
"--strict-markers", "--strict-markers",
"--ignore=docs_src",
] ]
xfail_strict = true xfail_strict = true
junit_family = "xunit2" junit_family = "xunit2"

4
requirements-tests.txt

@ -1,11 +1,13 @@
-e . -e .
pytest >=7.1.3,<8.0.0 pytest >=7.1.3,<8.0.0
coverage[toml] >= 6.5.0,< 8.0 coverage[toml] >= 6.5.0,< 8.0
dirty-equals >= 0.6.0
mypy ==1.3.0 mypy ==1.3.0
ruff ==0.0.272 ruff ==0.0.272
black == 23.3.0 black == 23.3.0
httpx >=0.23.0,<0.24.0 httpx >=0.23.0,<0.24.0
email_validator >=1.1.1,<2.0.0 email_validator >=2.0.0,<3.0.0
# TODO: once removing databases from tutorial, upgrade SQLAlchemy # TODO: once removing databases from tutorial, upgrade SQLAlchemy
# probably when including SQLModel # probably when including SQLModel
sqlalchemy >=1.3.18,<1.4.43 sqlalchemy >=1.3.18,<1.4.43

26
tests/test_additional_responses_custom_model_in_callback.py

@ -1,3 +1,4 @@
from dirty_equals import IsDict
from fastapi import APIRouter, FastAPI from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel, HttpUrl from pydantic import BaseModel, HttpUrl
@ -42,13 +43,24 @@ def test_openapi_schema():
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": { "schema": IsDict(
"title": "Callback Url", {
"maxLength": 2083, "title": "Callback Url",
"minLength": 1, "minLength": 1,
"type": "string", "type": "string",
"format": "uri", "format": "uri",
}, }
)
# TODO: remove when deprecating Pydantic v1
| IsDict(
{
"title": "Callback Url",
"maxLength": 2083,
"minLength": 1,
"type": "string",
"format": "uri",
}
),
"name": "callback_url", "name": "callback_url",
"in": "query", "in": "query",
} }

49
tests/test_annotated.py

@ -1,6 +1,8 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi import APIRouter, FastAPI, Query from fastapi import APIRouter, FastAPI, Query
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from typing_extensions import Annotated from typing_extensions import Annotated
app = FastAPI() app = FastAPI()
@ -30,21 +32,46 @@ client = TestClient(app)
foo_is_missing = { foo_is_missing = {
"detail": [ "detail": [
{ IsDict(
"loc": ["query", "foo"], {
"msg": "field required", "loc": ["query", "foo"],
"type": "value_error.missing", "msg": "Field required",
} "type": "missing",
"input": None,
"url": match_pydantic_error_url("missing"),
}
)
# TODO: remove when deprecating Pydantic v1
| IsDict(
{
"loc": ["query", "foo"],
"msg": "field required",
"type": "value_error.missing",
}
)
] ]
} }
foo_is_short = { foo_is_short = {
"detail": [ "detail": [
{ IsDict(
"ctx": {"limit_value": 1}, {
"loc": ["query", "foo"], "ctx": {"min_length": 1},
"msg": "ensure this value has at least 1 characters", "loc": ["query", "foo"],
"type": "value_error.any_str.min_length", "msg": "String should have at least 1 characters",
} "type": "string_too_short",
"input": "",
"url": match_pydantic_error_url("string_too_short"),
}
)
# TODO: remove when deprecating Pydantic v1
| IsDict(
{
"ctx": {"limit_value": 1},
"loc": ["query", "foo"],
"msg": "ensure this value has at least 1 characters",
"type": "value_error.any_str.min_length",
}
)
] ]
} }

23
tests/test_application.py

@ -1,4 +1,5 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from .main import app from .main import app
@ -266,10 +267,17 @@ def test_openapi_schema():
"operationId": "get_path_param_id_path_param__item_id__get", "operationId": "get_path_param_id_path_param__item_id__get",
"parameters": [ "parameters": [
{ {
"required": True,
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
"required": True,
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Item Id",
}
)
# TODO: remove when deprecating Pydantic v1
| IsDict({"title": "Item Id", "type": "string"}),
} }
], ],
} }
@ -969,10 +977,17 @@ def test_openapi_schema():
"operationId": "get_query_type_optional_query_int_optional_get", "operationId": "get_query_type_optional_query_int_optional_get",
"parameters": [ "parameters": [
{ {
"required": False,
"schema": {"title": "Query", "type": "integer"},
"name": "query", "name": "query",
"in": "query", "in": "query",
"required": False,
"schema": IsDict(
{
"anyOf": [{"type": "integer"}, {"type": "null"}],
"title": "Query",
}
)
# TODO: remove when deprecating Pydantic v1
| IsDict({"title": "Query", "type": "integer"}),
} }
], ],
} }

15
tests/test_custom_schema_fields.py

@ -1,4 +1,5 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel from pydantic import BaseModel
@ -8,10 +9,18 @@ app = FastAPI()
class Item(BaseModel): class Item(BaseModel):
name: str name: str
class Config: if PYDANTIC_V2:
schema_extra = { model_config = {
"x-something-internal": {"level": 4}, "json_schema_extra": {
"x-something-internal": {"level": 4},
}
} }
else:
class Config:
schema_extra = {
"x-something-internal": {"level": 4},
}
@app.get("/foo", response_model=Item) @app.get("/foo", response_model=Item)

53
tests/test_datetime_custom_encoder.py

@ -4,31 +4,54 @@ from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel from pydantic import BaseModel
from .utils import needs_pydanticv1, needs_pydanticv2
class ModelWithDatetimeField(BaseModel):
dt_field: datetime
class Config: @needs_pydanticv2
json_encoders = { def test_pydanticv2():
datetime: lambda dt: dt.replace( from pydantic import field_serializer
microsecond=0, tzinfo=timezone.utc
).isoformat()
}
class ModelWithDatetimeField(BaseModel):
dt_field: datetime
app = FastAPI() @field_serializer("dt_field")
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8)) def serialize_datetime(self, dt_field: datetime):
return dt_field.replace(microsecond=0, tzinfo=timezone.utc).isoformat()
app = FastAPI()
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
@app.get("/model", response_model=ModelWithDatetimeField) @app.get("/model", response_model=ModelWithDatetimeField)
def get_model(): def get_model():
return model return model
client = TestClient(app)
with client:
response = client.get("/model")
assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"}
# TODO: remove when deprecating Pydantic v1
@needs_pydanticv1
def test_pydanticv1():
class ModelWithDatetimeField(BaseModel):
dt_field: datetime
class Config:
json_encoders = {
datetime: lambda dt: dt.replace(
microsecond=0, tzinfo=timezone.utc
).isoformat()
}
client = TestClient(app) app = FastAPI()
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
@app.get("/model", response_model=ModelWithDatetimeField)
def get_model():
return model
def test_dt(): client = TestClient(app)
with client: with client:
response = client.get("/model") response = client.get("/model")
assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"} assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"}

35
tests/test_dependency_duplicates.py

@ -1,7 +1,9 @@
from typing import List from typing import List
from dirty_equals import IsDict
from fastapi import Depends, FastAPI from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from pydantic import BaseModel from pydantic import BaseModel
app = FastAPI() app = FastAPI()
@ -47,15 +49,30 @@ async def no_duplicates_sub(
def test_no_duplicates_invalid(): def test_no_duplicates_invalid():
response = client.post("/no-duplicates", json={"item": {"data": "myitem"}}) response = client.post("/no-duplicates", json={"item": {"data": "myitem"}})
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
assert response.json() == { assert response.json() == IsDict(
"detail": [ {
{ "detail": [
"loc": ["body", "item2"], {
"msg": "field required", "type": "missing",
"type": "value_error.missing", "loc": ["body", "item2"],
} "msg": "Field required",
] "input": None,
} "url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "item2"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_no_duplicates(): def test_no_duplicates():

666
tests/test_dependency_overrides.py

@ -1,8 +1,10 @@
from typing import Optional from typing import Optional
import pytest import pytest
from dirty_equals import IsDict
from fastapi import APIRouter, Depends, FastAPI from fastapi import APIRouter, Depends, FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
app = FastAPI() app = FastAPI()
@ -50,99 +52,180 @@ async def overrider_dependency_with_sub(msg: dict = Depends(overrider_sub_depend
return msg return msg
@pytest.mark.parametrize( def test_main_depends():
"url,status_code,expected", response = client.get("/main-depends/")
[ assert response.status_code == 422
( assert response.json() == IsDict(
"/main-depends/", {
422, "detail": [
{ {
"detail": [ "type": "missing",
{ "loc": ["query", "q"],
"loc": ["query", "q"], "msg": "Field required",
"msg": "field required", "input": None,
"type": "value_error.missing", "url": match_pydantic_error_url("missing"),
} }
] ]
}, }
), ) | IsDict(
( # TODO: remove when deprecating Pydantic v1
"/main-depends/?q=foo", {
200, "detail": [
{"in": "main-depends", "params": {"q": "foo", "skip": 0, "limit": 100}}, {
), "loc": ["query", "q"],
( "msg": "field required",
"/main-depends/?q=foo&skip=100&limit=200", "type": "value_error.missing",
200, }
{"in": "main-depends", "params": {"q": "foo", "skip": 100, "limit": 200}}, ]
), }
( )
"/decorator-depends/",
422,
{ def test_main_depends_q_foo():
"detail": [ response = client.get("/main-depends/?q=foo")
{ assert response.status_code == 200
"loc": ["query", "q"], assert response.json() == {
"msg": "field required", "in": "main-depends",
"type": "value_error.missing", "params": {"q": "foo", "skip": 0, "limit": 100},
} }
]
},
), def test_main_depends_q_foo_skip_100_limit_200():
("/decorator-depends/?q=foo", 200, {"in": "decorator-depends"}), response = client.get("/main-depends/?q=foo&skip=100&limit=200")
( assert response.status_code == 200
"/decorator-depends/?q=foo&skip=100&limit=200", assert response.json() == {
200, "in": "main-depends",
{"in": "decorator-depends"}, "params": {"q": "foo", "skip": 100, "limit": 200},
), }
(
"/router-depends/",
422, def test_decorator_depends():
{ response = client.get("/decorator-depends/")
"detail": [ assert response.status_code == 422
{ assert response.json() == IsDict(
"loc": ["query", "q"], {
"msg": "field required", "detail": [
"type": "value_error.missing", {
} "type": "missing",
] "loc": ["query", "q"],
}, "msg": "Field required",
), "input": None,
( "url": match_pydantic_error_url("missing"),
"/router-depends/?q=foo", }
200, ]
{"in": "router-depends", "params": {"q": "foo", "skip": 0, "limit": 100}}, }
), ) | IsDict(
( # TODO: remove when deprecating Pydantic v1
"/router-depends/?q=foo&skip=100&limit=200", {
200, "detail": [
{"in": "router-depends", "params": {"q": "foo", "skip": 100, "limit": 200}}, {
), "loc": ["query", "q"],
( "msg": "field required",
"/router-decorator-depends/", "type": "value_error.missing",
422, }
{ ]
"detail": [ }
{ )
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing", def test_decorator_depends_q_foo():
} response = client.get("/decorator-depends/?q=foo")
] assert response.status_code == 200
}, assert response.json() == {"in": "decorator-depends"}
),
("/router-decorator-depends/?q=foo", 200, {"in": "router-decorator-depends"}),
( def test_decorator_depends_q_foo_skip_100_limit_200():
"/router-decorator-depends/?q=foo&skip=100&limit=200", response = client.get("/decorator-depends/?q=foo&skip=100&limit=200")
200, assert response.status_code == 200
{"in": "router-decorator-depends"}, assert response.json() == {"in": "decorator-depends"}
),
],
) def test_router_depends():
def test_normal_app(url, status_code, expected): response = client.get("/router-depends/")
response = client.get(url) assert response.status_code == 422
assert response.status_code == status_code assert response.json() == IsDict(
assert response.json() == expected {
"detail": [
{
"type": "missing",
"loc": ["query", "q"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_router_depends_q_foo():
response = client.get("/router-depends/?q=foo")
assert response.status_code == 200
assert response.json() == {
"in": "router-depends",
"params": {"q": "foo", "skip": 0, "limit": 100},
}
def test_router_depends_q_foo_skip_100_limit_200():
response = client.get("/router-depends/?q=foo&skip=100&limit=200")
assert response.status_code == 200
assert response.json() == {
"in": "router-depends",
"params": {"q": "foo", "skip": 100, "limit": 200},
}
def test_router_decorator_depends():
response = client.get("/router-decorator-depends/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "q"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_router_decorator_depends_q_foo():
response = client.get("/router-decorator-depends/?q=foo")
assert response.status_code == 200
assert response.json() == {"in": "router-decorator-depends"}
def test_router_decorator_depends_q_foo_skip_100_limit_200():
response = client.get("/router-decorator-depends/?q=foo&skip=100&limit=200")
assert response.status_code == 200
assert response.json() == {"in": "router-decorator-depends"}
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -190,126 +273,281 @@ def test_override_simple(url, status_code, expected):
app.dependency_overrides = {} app.dependency_overrides = {}
@pytest.mark.parametrize( def test_override_with_sub_main_depends():
"url,status_code,expected",
[
(
"/main-depends/",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
(
"/main-depends/?q=foo",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
("/main-depends/?k=bar", 200, {"in": "main-depends", "params": {"k": "bar"}}),
(
"/decorator-depends/",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
(
"/decorator-depends/?q=foo",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
("/decorator-depends/?k=bar", 200, {"in": "decorator-depends"}),
(
"/router-depends/",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
(
"/router-depends/?q=foo",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
(
"/router-depends/?k=bar",
200,
{"in": "router-depends", "params": {"k": "bar"}},
),
(
"/router-decorator-depends/",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
(
"/router-decorator-depends/?q=foo",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
("/router-decorator-depends/?k=bar", 200, {"in": "router-decorator-depends"}),
],
)
def test_override_with_sub(url, status_code, expected):
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get(url) response = client.get("/main-depends/")
assert response.status_code == status_code assert response.status_code == 422
assert response.json() == expected assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub__main_depends_q_foo():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/main-depends/?q=foo")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub_main_depends_k_bar():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/main-depends/?k=bar")
assert response.status_code == 200
assert response.json() == {"in": "main-depends", "params": {"k": "bar"}}
app.dependency_overrides = {}
def test_override_with_sub_decorator_depends():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/decorator-depends/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub_decorator_depends_q_foo():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/decorator-depends/?q=foo")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub_decorator_depends_k_bar():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/decorator-depends/?k=bar")
assert response.status_code == 200
assert response.json() == {"in": "decorator-depends"}
app.dependency_overrides = {}
def test_override_with_sub_router_depends():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/router-depends/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub_router_depends_q_foo():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/router-depends/?q=foo")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub_router_depends_k_bar():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/router-depends/?k=bar")
assert response.status_code == 200
assert response.json() == {"in": "router-depends", "params": {"k": "bar"}}
app.dependency_overrides = {}
def test_override_with_sub_router_decorator_depends():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/router-decorator-depends/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub_router_decorator_depends_q_foo():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/router-decorator-depends/?q=foo")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub_router_decorator_depends_k_bar():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/router-decorator-depends/?k=bar")
assert response.status_code == 200
assert response.json() == {"in": "router-decorator-depends"}
app.dependency_overrides = {} app.dependency_overrides = {}

10
tests/test_extra_routes.py

@ -1,5 +1,6 @@
from typing import Optional from typing import Optional
from dirty_equals import IsDict
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
@ -327,7 +328,14 @@ def test_openapi_schema():
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"}, "price": IsDict(
{
"title": "Price",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
# TODO: remove when deprecating Pydantic v1
| IsDict({"title": "Price", "type": "number"}),
}, },
}, },
"ValidationError": { "ValidationError": {

0
tests/test_filter_pydantic_sub_model/__init__.py

35
tests/test_filter_pydantic_sub_model/app_pv1.py

@ -0,0 +1,35 @@
from typing import Optional
from fastapi import Depends, FastAPI
from pydantic import BaseModel, validator
app = FastAPI()
class ModelB(BaseModel):
username: str
class ModelC(ModelB):
password: str
class ModelA(BaseModel):
name: str
description: Optional[str] = None
model_b: ModelB
@validator("name")
def lower_username(cls, name: str, values):
if not name.endswith("A"):
raise ValueError("name must end in A")
return name
async def get_model_c() -> ModelC:
return ModelC(username="test-user", password="test-password")
@app.get("/model/{name}", response_model=ModelA)
async def get_model_a(name: str, model_c=Depends(get_model_c)):
return {"name": name, "description": "model-a-desc", "model_b": model_c}

52
tests/test_filter_pydantic_sub_model.py → tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py

@ -1,46 +1,20 @@
from typing import Optional
import pytest import pytest
from fastapi import Depends, FastAPI from fastapi.exceptions import ResponseValidationError
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel, ValidationError, validator
app = FastAPI()
class ModelB(BaseModel):
username: str
class ModelC(ModelB):
password: str
class ModelA(BaseModel):
name: str
description: Optional[str] = None
model_b: ModelB
@validator("name")
def lower_username(cls, name: str, values):
if not name.endswith("A"):
raise ValueError("name must end in A")
return name
async def get_model_c() -> ModelC:
return ModelC(username="test-user", password="test-password")
from ..utils import needs_pydanticv1
@app.get("/model/{name}", response_model=ModelA)
async def get_model_a(name: str, model_c=Depends(get_model_c)):
return {"name": name, "description": "model-a-desc", "model_b": model_c}
@pytest.fixture(name="client")
def get_client():
from .app_pv1 import app
client = TestClient(app) client = TestClient(app)
return client
def test_filter_sub_model(): @needs_pydanticv1
def test_filter_sub_model(client: TestClient):
response = client.get("/model/modelA") response = client.get("/model/modelA")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
assert response.json() == { assert response.json() == {
@ -50,8 +24,9 @@ def test_filter_sub_model():
} }
def test_validator_is_cloned(): @needs_pydanticv1
with pytest.raises(ValidationError) as err: def test_validator_is_cloned(client: TestClient):
with pytest.raises(ResponseValidationError) as err:
client.get("/model/modelX") client.get("/model/modelX")
assert err.value.errors() == [ assert err.value.errors() == [
{ {
@ -62,7 +37,8 @@ def test_validator_is_cloned():
] ]
def test_openapi_schema(): @needs_pydanticv1
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
assert response.json() == { assert response.json() == {

182
tests/test_filter_pydantic_sub_model_pv2.py

@ -0,0 +1,182 @@
from typing import Optional
import pytest
from dirty_equals import IsDict
from fastapi import Depends, FastAPI
from fastapi.exceptions import ResponseValidationError
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from .utils import needs_pydanticv2
@pytest.fixture(name="client")
def get_client():
from pydantic import BaseModel, FieldValidationInfo, field_validator
app = FastAPI()
class ModelB(BaseModel):
username: str
class ModelC(ModelB):
password: str
class ModelA(BaseModel):
name: str
description: Optional[str] = None
foo: ModelB
@field_validator("name")
def lower_username(cls, name: str, info: FieldValidationInfo):
if not name.endswith("A"):
raise ValueError("name must end in A")
return name
async def get_model_c() -> ModelC:
return ModelC(username="test-user", password="test-password")
@app.get("/model/{name}", response_model=ModelA)
async def get_model_a(name: str, model_c=Depends(get_model_c)):
return {"name": name, "description": "model-a-desc", "foo": model_c}
client = TestClient(app)
return client
@needs_pydanticv2
def test_filter_sub_model(client: TestClient):
response = client.get("/model/modelA")
assert response.status_code == 200, response.text
assert response.json() == {
"name": "modelA",
"description": "model-a-desc",
"foo": {"username": "test-user"},
}
@needs_pydanticv2
def test_validator_is_cloned(client: TestClient):
with pytest.raises(ResponseValidationError) as err:
client.get("/model/modelX")
assert err.value.errors() == [
IsDict(
{
"type": "value_error",
"loc": ("response", "name"),
"msg": "Value error, name must end in A",
"input": "modelX",
"ctx": {"error": "name must end in A"},
"url": match_pydantic_error_url("value_error"),
}
)
| IsDict(
# TODO remove when deprecating Pydantic v1
{
"loc": ("response", "name"),
"msg": "name must end in A",
"type": "value_error",
}
)
]
@needs_pydanticv2
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/model/{name}": {
"get": {
"summary": "Get Model A",
"operationId": "get_model_a_model__name__get",
"parameters": [
{
"required": True,
"schema": {"title": "Name", "type": "string"},
"name": "name",
"in": "path",
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/ModelA"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
}
},
"components": {
"schemas": {
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
"ModelA": {
"title": "ModelA",
"required": ["name", "foo"],
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"description": IsDict(
{
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
|
# TODO remove when deprecating Pydantic v1
IsDict({"title": "Description", "type": "string"}),
"foo": {"$ref": "#/components/schemas/ModelB"},
},
},
"ModelB": {
"title": "ModelB",
"required": ["username"],
"type": "object",
"properties": {"username": {"title": "Username", "type": "string"}},
},
"ValidationError": {
"title": "ValidationError",
"required": ["loc", "msg", "type"],
"type": "object",
"properties": {
"loc": {
"title": "Location",
"type": "array",
"items": {
"anyOf": [{"type": "string"}, {"type": "integer"}]
},
},
"msg": {"title": "Message", "type": "string"},
"type": {"title": "Error Type", "type": "string"},
},
},
}
},
}

277
tests/test_infer_param_optionality.py

@ -1,5 +1,6 @@
from typing import Optional from typing import Optional
from dirty_equals import IsDict
from fastapi import APIRouter, FastAPI from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
@ -104,35 +105,253 @@ def test_get_users_item():
assert response.json() == {"item_id": "item01", "user_id": "abc123"} assert response.json() == {"item_id": "item01", "user_id": "abc123"}
def test_schema_1(): def test_openapi_schema():
"""Check that the user_id is a required path parameter under /users"""
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
r = response.json() assert response.json() == {
"openapi": "3.0.2",
d = { "info": {"title": "FastAPI", "version": "0.1.0"},
"required": True, "paths": {
"schema": {"title": "User Id", "type": "string"}, "/users/": {
"name": "user_id", "get": {
"in": "path", "summary": "Get Users",
"operationId": "get_users_users__get",
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
}
},
"/users/{user_id}": {
"get": {
"summary": "Get User",
"operationId": "get_user_users__user_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "User Id", "type": "string"},
"name": "user_id",
"in": "path",
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/items/": {
"get": {
"summary": "Get Items",
"operationId": "get_items_items__get",
"parameters": [
{
"required": False,
"name": "user_id",
"in": "query",
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "User Id",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "User Id", "type": "string"}
),
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/items/{item_id}": {
"get": {
"summary": "Get Item",
"operationId": "get_item_items__item_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
},
{
"required": False,
"name": "user_id",
"in": "query",
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "User Id",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "User Id", "type": "string"}
),
},
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/users/{user_id}/items/": {
"get": {
"summary": "Get Items",
"operationId": "get_items_users__user_id__items__get",
"parameters": [
{
"required": True,
"name": "user_id",
"in": "path",
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "User Id",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "User Id", "type": "string"}
),
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/users/{user_id}/items/{item_id}": {
"get": {
"summary": "Get Item",
"operationId": "get_item_users__user_id__items__item_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
},
{
"required": True,
"name": "user_id",
"in": "path",
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "User Id",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "User Id", "type": "string"}
),
},
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
},
"components": {
"schemas": {
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
"ValidationError": {
"title": "ValidationError",
"required": ["loc", "msg", "type"],
"type": "object",
"properties": {
"loc": {
"title": "Location",
"type": "array",
"items": {
"anyOf": [{"type": "string"}, {"type": "integer"}]
},
},
"msg": {"title": "Message", "type": "string"},
"type": {"title": "Error Type", "type": "string"},
},
},
}
},
} }
assert d in r["paths"]["/users/{user_id}"]["get"]["parameters"]
assert d in r["paths"]["/users/{user_id}/items/"]["get"]["parameters"]
def test_schema_2():
"""Check that the user_id is an optional query parameter under /items"""
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
r = response.json()
d = {
"required": False,
"schema": {"title": "User Id", "type": "string"},
"name": "user_id",
"in": "query",
}
assert d in r["paths"]["/items/{item_id}"]["get"]["parameters"]
assert d in r["paths"]["/items/"]["get"]["parameters"]

86
tests/test_inherited_custom_class.py

@ -5,7 +5,7 @@ from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel from pydantic import BaseModel
app = FastAPI() from .utils import needs_pydanticv1, needs_pydanticv2
class MyUuid: class MyUuid:
@ -26,40 +26,78 @@ class MyUuid:
raise TypeError("vars() argument must have __dict__ attribute") raise TypeError("vars() argument must have __dict__ attribute")
@app.get("/fast_uuid") @needs_pydanticv2
def return_fast_uuid(): def test_pydanticv2():
# I don't want to import asyncpg for this test so I made my own UUID from pydantic import field_serializer
# Import asyncpg and uncomment the two lines below for the actual bug
# from asyncpg.pgproto import pgproto app = FastAPI()
# asyncpg_uuid = pgproto.UUID("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51") @app.get("/fast_uuid")
assert isinstance(asyncpg_uuid, uuid.UUID) def return_fast_uuid():
assert type(asyncpg_uuid) != uuid.UUID asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
with pytest.raises(TypeError): assert isinstance(asyncpg_uuid, uuid.UUID)
vars(asyncpg_uuid) assert type(asyncpg_uuid) != uuid.UUID
return {"fast_uuid": asyncpg_uuid} with pytest.raises(TypeError):
vars(asyncpg_uuid)
return {"fast_uuid": asyncpg_uuid}
class SomeCustomClass(BaseModel):
model_config = {"arbitrary_types_allowed": True}
class SomeCustomClass(BaseModel): a_uuid: MyUuid
class Config:
arbitrary_types_allowed = True
json_encoders = {uuid.UUID: str}
a_uuid: MyUuid @field_serializer("a_uuid")
def serialize_a_uuid(self, v):
return str(v)
@app.get("/get_custom_class")
def return_some_user():
# Test that the fix also works for custom pydantic classes
return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
@app.get("/get_custom_class") client = TestClient(app)
def return_some_user():
# Test that the fix also works for custom pydantic classes
return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
with client:
response_simple = client.get("/fast_uuid")
response_pydantic = client.get("/get_custom_class")
assert response_simple.json() == {
"fast_uuid": "a10ff360-3b1e-4984-a26f-d3ab460bdb51"
}
assert response_pydantic.json() == {
"a_uuid": "b8799909-f914-42de-91bc-95c819218d01"
}
# TODO: remove when deprecating Pydantic v1
@needs_pydanticv1
def test_pydanticv1():
app = FastAPI()
@app.get("/fast_uuid")
def return_fast_uuid():
asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
assert isinstance(asyncpg_uuid, uuid.UUID)
assert type(asyncpg_uuid) != uuid.UUID
with pytest.raises(TypeError):
vars(asyncpg_uuid)
return {"fast_uuid": asyncpg_uuid}
class SomeCustomClass(BaseModel):
class Config:
arbitrary_types_allowed = True
json_encoders = {uuid.UUID: str}
a_uuid: MyUuid
client = TestClient(app) @app.get("/get_custom_class")
def return_some_user():
# Test that the fix also works for custom pydantic classes
return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
client = TestClient(app)
def test_dt():
with client: with client:
response_simple = client.get("/fast_uuid") response_simple = client.get("/fast_uuid")
response_pydantic = client.get("/get_custom_class") response_pydantic = client.get("/get_custom_class")

139
tests/test_jsonable_encoder.py

@ -5,8 +5,11 @@ from pathlib import PurePath, PurePosixPath, PureWindowsPath
from typing import Optional from typing import Optional
import pytest import pytest
from fastapi._compat import PYDANTIC_V2
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel, Field, ValidationError, create_model from pydantic import BaseModel, Field, ValidationError
from .utils import needs_pydanticv1, needs_pydanticv2
class Person: class Person:
@ -45,22 +48,6 @@ class Unserializable:
raise NotImplementedError() raise NotImplementedError()
class ModelWithCustomEncoder(BaseModel):
dt_field: datetime
class Config:
json_encoders = {
datetime: lambda dt: dt.replace(
microsecond=0, tzinfo=timezone.utc
).isoformat()
}
class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder):
class Config:
pass
class RoleEnum(Enum): class RoleEnum(Enum):
admin = "admin" admin = "admin"
normal = "normal" normal = "normal"
@ -69,8 +56,12 @@ class RoleEnum(Enum):
class ModelWithConfig(BaseModel): class ModelWithConfig(BaseModel):
role: Optional[RoleEnum] = None role: Optional[RoleEnum] = None
class Config: if PYDANTIC_V2:
use_enum_values = True model_config = {"use_enum_values": True}
else:
class Config:
use_enum_values = True
class ModelWithAlias(BaseModel): class ModelWithAlias(BaseModel):
@ -83,23 +74,6 @@ class ModelWithDefault(BaseModel):
bla: str = "bla" bla: str = "bla"
class ModelWithRoot(BaseModel):
__root__: str
@pytest.fixture(
name="model_with_path", params=[PurePath, PurePosixPath, PureWindowsPath]
)
def fixture_model_with_path(request):
class Config:
arbitrary_types_allowed = True
ModelWithPath = create_model(
"ModelWithPath", path=(request.param, ...), __config__=Config # type: ignore
)
return ModelWithPath(path=request.param("/foo", "bar"))
def test_encode_dict(): def test_encode_dict():
pet = {"name": "Firulais", "owner": {"name": "Foo"}} pet = {"name": "Firulais", "owner": {"name": "Foo"}}
assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}} assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}}
@ -153,14 +127,47 @@ def test_encode_unsupported():
jsonable_encoder(unserializable) jsonable_encoder(unserializable)
def test_encode_custom_json_encoders_model(): @needs_pydanticv2
def test_encode_custom_json_encoders_model_pydanticv2():
from pydantic import field_serializer
class ModelWithCustomEncoder(BaseModel):
dt_field: datetime
@field_serializer("dt_field")
def serialize_dt_field(self, dt):
return dt.replace(microsecond=0, tzinfo=timezone.utc).isoformat()
class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder):
pass
model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8)) model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8))
assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"} assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8))
assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
def test_encode_custom_json_encoders_model_subclass(): # TODO: remove when deprecating Pydantic v1
model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8)) @needs_pydanticv1
def test_encode_custom_json_encoders_model_pydanticv1():
class ModelWithCustomEncoder(BaseModel):
dt_field: datetime
class Config:
json_encoders = {
datetime: lambda dt: dt.replace(
microsecond=0, tzinfo=timezone.utc
).isoformat()
}
class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder):
class Config:
pass
model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8))
assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"} assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8))
assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
def test_encode_model_with_config(): def test_encode_model_with_config():
@ -196,6 +203,7 @@ def test_encode_model_with_default():
} }
@needs_pydanticv1
def test_custom_encoders(): def test_custom_encoders():
class safe_datetime(datetime): class safe_datetime(datetime):
pass pass
@ -226,14 +234,55 @@ def test_custom_enum_encoders():
assert encoded_instance == custom_enum_encoder(instance) assert encoded_instance == custom_enum_encoder(instance)
def test_encode_model_with_path(model_with_path): def test_encode_model_with_pure_path():
if isinstance(model_with_path.path, PureWindowsPath): class ModelWithPath(BaseModel):
expected = "\\foo\\bar" path: PurePath
else:
expected = "/foo/bar" if PYDANTIC_V2:
assert jsonable_encoder(model_with_path) == {"path": expected} model_config = {"arbitrary_types_allowed": True}
else:
class Config:
arbitrary_types_allowed = True
obj = ModelWithPath(path=PurePath("/foo", "bar"))
assert jsonable_encoder(obj) == {"path": "/foo/bar"}
def test_encode_model_with_pure_posix_path():
class ModelWithPath(BaseModel):
path: PurePosixPath
if PYDANTIC_V2:
model_config = {"arbitrary_types_allowed": True}
else:
class Config:
arbitrary_types_allowed = True
obj = ModelWithPath(path=PurePosixPath("/foo", "bar"))
assert jsonable_encoder(obj) == {"path": "/foo/bar"}
def test_encode_model_with_pure_windows_path():
class ModelWithPath(BaseModel):
path: PureWindowsPath
if PYDANTIC_V2:
model_config = {"arbitrary_types_allowed": True}
else:
class Config:
arbitrary_types_allowed = True
obj = ModelWithPath(path=PureWindowsPath("/foo", "bar"))
assert jsonable_encoder(obj) == {"path": "\\foo\\bar"}
@needs_pydanticv1
def test_encode_root(): def test_encode_root():
class ModelWithRoot(BaseModel):
__root__: str
model = ModelWithRoot(__root__="Foo") model = ModelWithRoot(__root__="Foo")
assert jsonable_encoder(model) == "Foo" assert jsonable_encoder(model) == "Foo"

160
tests/test_multi_body_errors.py

@ -1,8 +1,10 @@
from decimal import Decimal from decimal import Decimal
from typing import List from typing import List
from dirty_equals import IsDict, IsOneOf
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from pydantic import BaseModel, condecimal from pydantic import BaseModel, condecimal
app = FastAPI() app = FastAPI()
@ -21,59 +23,115 @@ def save_item_no_body(item: List[Item]):
client = TestClient(app) client = TestClient(app)
single_error = {
"detail": [
{
"ctx": {"limit_value": 0.0},
"loc": ["body", 0, "age"],
"msg": "ensure this value is greater than 0",
"type": "value_error.number.not_gt",
}
]
}
multiple_errors = {
"detail": [
{
"loc": ["body", 0, "name"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", 0, "age"],
"msg": "value is not a valid decimal",
"type": "type_error.decimal",
},
{
"loc": ["body", 1, "name"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", 1, "age"],
"msg": "value is not a valid decimal",
"type": "type_error.decimal",
},
]
}
def test_put_correct_body(): def test_put_correct_body():
response = client.post("/items/", json=[{"name": "Foo", "age": 5}]) response = client.post("/items/", json=[{"name": "Foo", "age": 5}])
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
assert response.json() == {"item": [{"name": "Foo", "age": 5}]} assert response.json() == {
"item": [
{
"name": "Foo",
"age": IsOneOf(
5,
# TODO: remove when deprecating Pydantic v1
"5",
),
}
]
}
def test_jsonable_encoder_requiring_error(): def test_jsonable_encoder_requiring_error():
response = client.post("/items/", json=[{"name": "Foo", "age": -1.0}]) response = client.post("/items/", json=[{"name": "Foo", "age": -1.0}])
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
assert response.json() == single_error assert response.json() == IsDict(
{
"detail": [
{
"type": "greater_than",
"loc": ["body", 0, "age"],
"msg": "Input should be greater than 0",
"input": -1.0,
"ctx": {"gt": 0.0},
"url": match_pydantic_error_url("greater_than"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"ctx": {"limit_value": 0.0},
"loc": ["body", 0, "age"],
"msg": "ensure this value is greater than 0",
"type": "value_error.number.not_gt",
}
]
}
)
def test_put_incorrect_body_multiple(): def test_put_incorrect_body_multiple():
response = client.post("/items/", json=[{"age": "five"}, {"age": "six"}]) response = client.post("/items/", json=[{"age": "five"}, {"age": "six"}])
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
assert response.json() == multiple_errors assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", 0, "name"],
"msg": "Field required",
"input": {"age": "five"},
"url": match_pydantic_error_url("missing"),
},
{
"type": "decimal_parsing",
"loc": ["body", 0, "age"],
"msg": "Input should be a valid decimal",
"input": "five",
},
{
"type": "missing",
"loc": ["body", 1, "name"],
"msg": "Field required",
"input": {"age": "six"},
"url": match_pydantic_error_url("missing"),
},
{
"type": "decimal_parsing",
"loc": ["body", 1, "age"],
"msg": "Input should be a valid decimal",
"input": "six",
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", 0, "name"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", 0, "age"],
"msg": "value is not a valid decimal",
"type": "type_error.decimal",
},
{
"loc": ["body", 1, "name"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", 1, "age"],
"msg": "value is not a valid decimal",
"type": "type_error.decimal",
},
]
}
)
def test_openapi_schema(): def test_openapi_schema():
@ -126,11 +184,23 @@ def test_openapi_schema():
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"age": { "age": IsDict(
"title": "Age", {
"exclusiveMinimum": 0.0, "title": "Age",
"type": "number", "anyOf": [
}, {"exclusiveMinimum": 0.0, "type": "number"},
{"type": "string"},
],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "Age",
"exclusiveMinimum": 0.0,
"type": "number",
}
),
}, },
}, },
"ValidationError": { "ValidationError": {

55
tests/test_multi_query_errors.py

@ -1,7 +1,9 @@
from typing import List from typing import List
from dirty_equals import IsDict
from fastapi import FastAPI, Query from fastapi import FastAPI, Query
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
app = FastAPI() app = FastAPI()
@ -14,22 +16,6 @@ def read_items(q: List[int] = Query(default=None)):
client = TestClient(app) client = TestClient(app)
multiple_errors = {
"detail": [
{
"loc": ["query", "q", 0],
"msg": "value is not a valid integer",
"type": "type_error.integer",
},
{
"loc": ["query", "q", 1],
"msg": "value is not a valid integer",
"type": "type_error.integer",
},
]
}
def test_multi_query(): def test_multi_query():
response = client.get("/items/?q=5&q=6") response = client.get("/items/?q=5&q=6")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
@ -39,7 +25,42 @@ def test_multi_query():
def test_multi_query_incorrect(): def test_multi_query_incorrect():
response = client.get("/items/?q=five&q=six") response = client.get("/items/?q=five&q=six")
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
assert response.json() == multiple_errors assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"loc": ["query", "q", 0],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "five",
"url": match_pydantic_error_url("int_parsing"),
},
{
"type": "int_parsing",
"loc": ["query", "q", 1],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "six",
"url": match_pydantic_error_url("int_parsing"),
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "q", 0],
"msg": "value is not a valid integer",
"type": "type_error.integer",
},
{
"loc": ["query", "q", 1],
"msg": "value is not a valid integer",
"type": "type_error.integer",
},
]
}
)
def test_openapi_schema(): def test_openapi_schema():

21
tests/test_openapi_query_parameter_extension.py

@ -1,5 +1,6 @@
from typing import Optional from typing import Optional
from dirty_equals import IsDict
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
@ -52,11 +53,21 @@ def test_openapi():
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": { "schema": IsDict(
"title": "Standard Query Param", {
"type": "integer", "anyOf": [{"type": "integer"}, {"type": "null"}],
"default": 50, "default": 50,
}, "title": "Standard Query Param",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "Standard Query Param",
"type": "integer",
"default": 50,
}
),
"name": "standard_query_param", "name": "standard_query_param",
"in": "query", "in": "query",
}, },

15
tests/test_openapi_servers.py

@ -1,3 +1,4 @@
from dirty_equals import IsOneOf
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
@ -35,10 +36,20 @@ def test_openapi_schema():
"servers": [ "servers": [
{"url": "/", "description": "Default, relative server"}, {"url": "/", "description": "Default, relative server"},
{ {
"url": "http://staging.localhost.tiangolo.com:8000", "url": IsOneOf(
"http://staging.localhost.tiangolo.com:8000/",
# TODO: remove when deprecating Pydantic v1
"http://staging.localhost.tiangolo.com:8000",
),
"description": "Staging but actually localhost still", "description": "Staging but actually localhost still",
}, },
{"url": "https://prod.example.com"}, {
"url": IsOneOf(
"https://prod.example.com/",
# TODO: remove when deprecating Pydantic v1
"https://prod.example.com",
)
},
], ],
"paths": { "paths": {
"/foo": { "/foo": {

135
tests/test_params_repr.py

@ -1,6 +1,6 @@
from typing import Any, List from typing import Any, List
import pytest from dirty_equals import IsOneOf
from fastapi.params import Body, Cookie, Depends, Header, Param, Path, Query from fastapi.params import Body, Cookie, Depends, Header, Param, Path, Query
test_data: List[Any] = ["teststr", None, ..., 1, []] test_data: List[Any] = ["teststr", None, ..., 1, []]
@ -10,34 +10,137 @@ def get_user():
return {} # pragma: no cover return {} # pragma: no cover
@pytest.fixture(scope="function", params=test_data) def test_param_repr_str():
def params(request): assert repr(Param("teststr")) == "Param(teststr)"
return request.param
def test_param_repr(params): def test_param_repr_none():
assert repr(Param(params)) == "Param(" + str(params) + ")" assert repr(Param(None)) == "Param(None)"
def test_param_repr_ellipsis():
assert repr(Param(...)) == IsOneOf(
"Param(PydanticUndefined)",
# TODO: remove when deprecating Pydantic v1
"Param(Ellipsis)",
)
def test_param_repr_number():
assert repr(Param(1)) == "Param(1)"
def test_param_repr_list():
assert repr(Param([])) == "Param([])"
def test_path_repr(): def test_path_repr():
assert repr(Path()) == "Path(Ellipsis)" assert repr(Path()) == IsOneOf(
assert repr(Path(...)) == "Path(Ellipsis)" "Path(PydanticUndefined)",
# TODO: remove when deprecating Pydantic v1
"Path(Ellipsis)",
)
assert repr(Path(...)) == IsOneOf(
"Path(PydanticUndefined)",
# TODO: remove when deprecating Pydantic v1
"Path(Ellipsis)",
)
def test_query_repr(params): def test_query_repr_str():
assert repr(Query(params)) == "Query(" + str(params) + ")" assert repr(Query("teststr")) == "Query(teststr)"
def test_header_repr(params): def test_query_repr_none():
assert repr(Header(params)) == "Header(" + str(params) + ")" assert repr(Query(None)) == "Query(None)"
def test_query_repr_ellipsis():
assert repr(Query(...)) == IsOneOf(
"Query(PydanticUndefined)",
# TODO: remove when deprecating Pydantic v1
"Query(Ellipsis)",
)
def test_query_repr_number():
assert repr(Query(1)) == "Query(1)"
def test_query_repr_list():
assert repr(Query([])) == "Query([])"
def test_header_repr_str():
assert repr(Header("teststr")) == "Header(teststr)"
def test_header_repr_none():
assert repr(Header(None)) == "Header(None)"
def test_header_repr_ellipsis():
assert repr(Header(...)) == IsOneOf(
"Header(PydanticUndefined)",
# TODO: remove when deprecating Pydantic v1
"Header(Ellipsis)",
)
def test_header_repr_number():
assert repr(Header(1)) == "Header(1)"
def test_header_repr_list():
assert repr(Header([])) == "Header([])"
def test_cookie_repr_str():
assert repr(Cookie("teststr")) == "Cookie(teststr)"
def test_cookie_repr_none():
assert repr(Cookie(None)) == "Cookie(None)"
def test_cookie_repr_ellipsis():
assert repr(Cookie(...)) == IsOneOf(
"Cookie(PydanticUndefined)",
# TODO: remove when deprecating Pydantic v1
"Cookie(Ellipsis)",
)
def test_cookie_repr_number():
assert repr(Cookie(1)) == "Cookie(1)"
def test_cookie_repr_list():
assert repr(Cookie([])) == "Cookie([])"
def test_body_repr_str():
assert repr(Body("teststr")) == "Body(teststr)"
def test_body_repr_none():
assert repr(Body(None)) == "Body(None)"
def test_body_repr_ellipsis():
assert repr(Body(...)) == IsOneOf(
"Body(PydanticUndefined)",
# TODO: remove when deprecating Pydantic v1
"Body(Ellipsis)",
)
def test_cookie_repr(params): def test_body_repr_number():
assert repr(Cookie(params)) == "Cookie(" + str(params) + ")" assert repr(Body(1)) == "Body(1)"
def test_body_repr(params): def test_body_repr_list():
assert repr(Body(params)) == "Body(" + str(params) + ")" assert repr(Body([])) == "Body([])"
def test_depends_repr(): def test_depends_repr():

1362
tests/test_path.py

File diff suppressed because it is too large

460
tests/test_query.py

@ -1,62 +1,410 @@
import pytest from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from .main import app from .main import app
client = TestClient(app) client = TestClient(app)
response_missing = {
"detail": [ def test_query():
{ response = client.get("/query")
"loc": ["query", "query"], assert response.status_code == 422
"msg": "field required", assert response.json() == IsDict(
"type": "value_error.missing", {
} "detail": [
] {
} "type": "missing",
"loc": ["query", "query"],
response_not_valid_int = { "msg": "Field required",
"detail": [ "input": None,
{ "url": match_pydantic_error_url("missing"),
"loc": ["query", "query"], }
"msg": "value is not a valid integer", ]
"type": "type_error.integer", }
} ) | IsDict(
] # TODO: remove when deprecating Pydantic v1
} {
"detail": [
{
@pytest.mark.parametrize( "loc": ["query", "query"],
"path,expected_status,expected_response", "msg": "field required",
[ "type": "value_error.missing",
("/query", 422, response_missing), }
("/query?query=baz", 200, "foo bar baz"), ]
("/query?not_declared=baz", 422, response_missing), }
("/query/optional", 200, "foo bar"), )
("/query/optional?query=baz", 200, "foo bar baz"),
("/query/optional?not_declared=baz", 200, "foo bar"),
("/query/int", 422, response_missing), def test_query_query_baz():
("/query/int?query=42", 200, "foo bar 42"), response = client.get("/query?query=baz")
("/query/int?query=42.5", 422, response_not_valid_int), assert response.status_code == 200
("/query/int?query=baz", 422, response_not_valid_int), assert response.json() == "foo bar baz"
("/query/int?not_declared=baz", 422, response_missing),
("/query/int/optional", 200, "foo bar"),
("/query/int/optional?query=50", 200, "foo bar 50"), def test_query_not_declared_baz():
("/query/int/optional?query=foo", 422, response_not_valid_int), response = client.get("/query?not_declared=baz")
("/query/int/default", 200, "foo bar 10"), assert response.status_code == 422
("/query/int/default?query=50", 200, "foo bar 50"), assert response.json() == IsDict(
("/query/int/default?query=foo", 422, response_not_valid_int), {
("/query/param", 200, "foo bar"), "detail": [
("/query/param?query=50", 200, "foo bar 50"), {
("/query/param-required", 422, response_missing), "type": "missing",
("/query/param-required?query=50", 200, "foo bar 50"), "loc": ["query", "query"],
("/query/param-required/int", 422, response_missing), "msg": "Field required",
("/query/param-required/int?query=50", 200, "foo bar 50"), "input": None,
("/query/param-required/int?query=foo", 422, response_not_valid_int), "url": match_pydantic_error_url("missing"),
("/query/frozenset/?query=1&query=1&query=2", 200, "1,2"), }
], ]
) }
def test_get_path(path, expected_status, expected_response): ) | IsDict(
response = client.get(path) # TODO: remove when deprecating Pydantic v1
assert response.status_code == expected_status {
assert response.json() == expected_response "detail": [
{
"loc": ["query", "query"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_query_optional():
response = client.get("/query/optional")
assert response.status_code == 200
assert response.json() == "foo bar"
def test_query_optional_query_baz():
response = client.get("/query/optional?query=baz")
assert response.status_code == 200
assert response.json() == "foo bar baz"
def test_query_optional_not_declared_baz():
response = client.get("/query/optional?not_declared=baz")
assert response.status_code == 200
assert response.json() == "foo bar"
def test_query_int():
response = client.get("/query/int")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "query"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_query_int_query_42():
response = client.get("/query/int?query=42")
assert response.status_code == 200
assert response.json() == "foo bar 42"
def test_query_int_query_42_5():
response = client.get("/query/int?query=42.5")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"loc": ["query", "query"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "42.5",
"url": match_pydantic_error_url("int_parsing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
def test_query_int_query_baz():
response = client.get("/query/int?query=baz")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"loc": ["query", "query"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "baz",
"url": match_pydantic_error_url("int_parsing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
def test_query_int_not_declared_baz():
response = client.get("/query/int?not_declared=baz")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "query"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_query_int_optional():
response = client.get("/query/int/optional")
assert response.status_code == 200
assert response.json() == "foo bar"
def test_query_int_optional_query_50():
response = client.get("/query/int/optional?query=50")
assert response.status_code == 200
assert response.json() == "foo bar 50"
def test_query_int_optional_query_foo():
response = client.get("/query/int/optional?query=foo")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"loc": ["query", "query"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "foo",
"url": match_pydantic_error_url("int_parsing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
def test_query_int_default():
response = client.get("/query/int/default")
assert response.status_code == 200
assert response.json() == "foo bar 10"
def test_query_int_default_query_50():
response = client.get("/query/int/default?query=50")
assert response.status_code == 200
assert response.json() == "foo bar 50"
def test_query_int_default_query_foo():
response = client.get("/query/int/default?query=foo")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"loc": ["query", "query"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "foo",
"url": match_pydantic_error_url("int_parsing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
def test_query_param():
response = client.get("/query/param")
assert response.status_code == 200
assert response.json() == "foo bar"
def test_query_param_query_50():
response = client.get("/query/param?query=50")
assert response.status_code == 200
assert response.json() == "foo bar 50"
def test_query_param_required():
response = client.get("/query/param-required")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "query"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_query_param_required_query_50():
response = client.get("/query/param-required?query=50")
assert response.status_code == 200
assert response.json() == "foo bar 50"
def test_query_param_required_int():
response = client.get("/query/param-required/int")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "query"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_query_param_required_int_query_50():
response = client.get("/query/param-required/int?query=50")
assert response.status_code == 200
assert response.json() == "foo bar 50"
def test_query_param_required_int_query_foo():
response = client.get("/query/param-required/int?query=foo")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"loc": ["query", "query"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "foo",
"url": match_pydantic_error_url("int_parsing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
def test_query_frozenset_query_1_query_1_query_2():
response = client.get("/query/frozenset/?query=1&query=1&query=2")
assert response.status_code == 200
assert response.json() == "1,2"

83
tests/test_read_with_orm_mode.py

@ -2,48 +2,83 @@ from typing import Any
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel from pydantic import BaseModel, ConfigDict
from .utils import needs_pydanticv1, needs_pydanticv2
class PersonBase(BaseModel):
name: str
lastname: str
@needs_pydanticv2
def test_read_with_orm_mode() -> None:
class PersonBase(BaseModel):
name: str
lastname: str
class Person(PersonBase):
@property
def full_name(self) -> str:
return f"{self.name} {self.lastname}"
model_config = ConfigDict(from_attributes=True)
class PersonCreate(PersonBase):
pass
class Person(PersonBase): class PersonRead(PersonBase):
@property full_name: str
def full_name(self) -> str:
return f"{self.name} {self.lastname}"
class Config: model_config = {"from_attributes": True}
orm_mode = True
read_with_orm_mode = True
app = FastAPI()
class PersonCreate(PersonBase): @app.post("/people/", response_model=PersonRead)
pass def create_person(person: PersonCreate) -> Any:
db_person = Person.model_validate(person)
return db_person
client = TestClient(app)
person_data = {"name": "Dive", "lastname": "Wilson"}
response = client.post("/people/", json=person_data)
data = response.json()
assert response.status_code == 200, response.text
assert data["name"] == person_data["name"]
assert data["lastname"] == person_data["lastname"]
assert data["full_name"] == person_data["name"] + " " + person_data["lastname"]
class PersonRead(PersonBase): @needs_pydanticv1
full_name: str def test_read_with_orm_mode_pv1() -> None:
class PersonBase(BaseModel):
name: str
lastname: str
class Config: class Person(PersonBase):
orm_mode = True @property
def full_name(self) -> str:
return f"{self.name} {self.lastname}"
class Config:
orm_mode = True
read_with_orm_mode = True
app = FastAPI() class PersonCreate(PersonBase):
pass
class PersonRead(PersonBase):
full_name: str
@app.post("/people/", response_model=PersonRead) class Config:
def create_person(person: PersonCreate) -> Any: orm_mode = True
db_person = Person.from_orm(person)
return db_person
app = FastAPI()
client = TestClient(app) @app.post("/people/", response_model=PersonRead)
def create_person(person: PersonCreate) -> Any:
db_person = Person.from_orm(person)
return db_person
client = TestClient(app)
def test_read_with_orm_mode() -> None:
person_data = {"name": "Dive", "lastname": "Wilson"} person_data = {"name": "Dive", "lastname": "Wilson"}
response = client.post("/people/", json=person_data) response = client.post("/people/", json=person_data)
data = response.json() data = response.json()

1
tests/test_request_body_parameters_media_type.py

@ -39,7 +39,6 @@ client = TestClient(app)
def test_openapi_schema(): def test_openapi_schema():
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
# insert_assert(response.json())
assert response.json() == { assert response.json() == {
"openapi": "3.0.2", "openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"}, "info": {"title": "FastAPI", "version": "0.1.0"},

28
tests/test_response_by_alias.py

@ -1,8 +1,9 @@
from typing import List from typing import List
from fastapi import FastAPI from fastapi import FastAPI
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel, Field from pydantic import BaseModel, ConfigDict, Field
app = FastAPI() app = FastAPI()
@ -14,13 +15,24 @@ class Model(BaseModel):
class ModelNoAlias(BaseModel): class ModelNoAlias(BaseModel):
name: str name: str
class Config: if PYDANTIC_V2:
schema_extra = { model_config = ConfigDict(
"description": ( json_schema_extra={
"response_model_by_alias=False is basically a quick hack, to support " "description": (
"proper OpenAPI use another model with the correct field names" "response_model_by_alias=False is basically a quick hack, to support "
) "proper OpenAPI use another model with the correct field names"
} )
}
)
else:
class Config:
schema_extra = {
"description": (
"response_model_by_alias=False is basically a quick hack, to support "
"proper OpenAPI use another model with the correct field names"
)
}
@app.get("/dict", response_model=Model, response_model_by_alias=False) @app.get("/dict", response_model=Model, response_model_by_alias=False)

16
tests/test_response_model_as_return_annotation.py

@ -2,10 +2,10 @@ from typing import List, Union
import pytest import pytest
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.exceptions import FastAPIError from fastapi.exceptions import FastAPIError, ResponseValidationError
from fastapi.responses import JSONResponse, Response from fastapi.responses import JSONResponse, Response
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel, ValidationError from pydantic import BaseModel
class BaseUser(BaseModel): class BaseUser(BaseModel):
@ -277,12 +277,12 @@ def test_response_model_no_annotation_return_exact_dict():
def test_response_model_no_annotation_return_invalid_dict(): def test_response_model_no_annotation_return_invalid_dict():
with pytest.raises(ValidationError): with pytest.raises(ResponseValidationError):
client.get("/response_model-no_annotation-return_invalid_dict") client.get("/response_model-no_annotation-return_invalid_dict")
def test_response_model_no_annotation_return_invalid_model(): def test_response_model_no_annotation_return_invalid_model():
with pytest.raises(ValidationError): with pytest.raises(ResponseValidationError):
client.get("/response_model-no_annotation-return_invalid_model") client.get("/response_model-no_annotation-return_invalid_model")
@ -313,12 +313,12 @@ def test_no_response_model_annotation_return_exact_dict():
def test_no_response_model_annotation_return_invalid_dict(): def test_no_response_model_annotation_return_invalid_dict():
with pytest.raises(ValidationError): with pytest.raises(ResponseValidationError):
client.get("/no_response_model-annotation-return_invalid_dict") client.get("/no_response_model-annotation-return_invalid_dict")
def test_no_response_model_annotation_return_invalid_model(): def test_no_response_model_annotation_return_invalid_model():
with pytest.raises(ValidationError): with pytest.raises(ResponseValidationError):
client.get("/no_response_model-annotation-return_invalid_model") client.get("/no_response_model-annotation-return_invalid_model")
@ -395,12 +395,12 @@ def test_response_model_model1_annotation_model2_return_exact_dict():
def test_response_model_model1_annotation_model2_return_invalid_dict(): def test_response_model_model1_annotation_model2_return_invalid_dict():
with pytest.raises(ValidationError): with pytest.raises(ResponseValidationError):
client.get("/response_model_model1-annotation_model2-return_invalid_dict") client.get("/response_model_model1-annotation_model2-return_invalid_dict")
def test_response_model_model1_annotation_model2_return_invalid_model(): def test_response_model_model1_annotation_model2_return_invalid_model():
with pytest.raises(ValidationError): with pytest.raises(ResponseValidationError):
client.get("/response_model_model1-annotation_model2-return_invalid_model") client.get("/response_model_model1-annotation_model2-return_invalid_model")

79
tests/test_response_model_data_filter.py

@ -0,0 +1,79 @@
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class UserBase(BaseModel):
email: str
class UserCreate(UserBase):
password: str
class UserDB(UserBase):
hashed_password: str
class PetDB(BaseModel):
name: str
owner: UserDB
class PetOut(BaseModel):
name: str
owner: UserBase
@app.post("/users/", response_model=UserBase)
async def create_user(user: UserCreate):
return user
@app.get("/pets/{pet_id}", response_model=PetOut)
async def read_pet(pet_id: int):
user = UserDB(
email="[email protected]",
hashed_password="secrethashed",
)
pet = PetDB(name="Nibbler", owner=user)
return pet
@app.get("/pets/", response_model=list[PetOut])
async def read_pets():
user = UserDB(
email="[email protected]",
hashed_password="secrethashed",
)
pet1 = PetDB(name="Nibbler", owner=user)
pet2 = PetDB(name="Zoidberg", owner=user)
return [pet1, pet2]
client = TestClient(app)
def test_filter_top_level_model():
response = client.post(
"/users", json={"email": "[email protected]", "password": "secret"}
)
assert response.json() == {"email": "[email protected]"}
def test_filter_second_level_model():
response = client.get("/pets/1")
assert response.json() == {
"name": "Nibbler",
"owner": {"email": "[email protected]"},
}
def test_list_of_models():
response = client.get("/pets/")
assert response.json() == [
{"name": "Nibbler", "owner": {"email": "[email protected]"}},
{"name": "Zoidberg", "owner": {"email": "[email protected]"}},
]

81
tests/test_response_model_data_filter_no_inheritance.py

@ -0,0 +1,81 @@
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class UserCreate(BaseModel):
email: str
password: str
class UserDB(BaseModel):
email: str
hashed_password: str
class User(BaseModel):
email: str
class PetDB(BaseModel):
name: str
owner: UserDB
class PetOut(BaseModel):
name: str
owner: User
@app.post("/users/", response_model=User)
async def create_user(user: UserCreate):
return user
@app.get("/pets/{pet_id}", response_model=PetOut)
async def read_pet(pet_id: int):
user = UserDB(
email="[email protected]",
hashed_password="secrethashed",
)
pet = PetDB(name="Nibbler", owner=user)
return pet
@app.get("/pets/", response_model=list[PetOut])
async def read_pets():
user = UserDB(
email="[email protected]",
hashed_password="secrethashed",
)
pet1 = PetDB(name="Nibbler", owner=user)
pet2 = PetDB(name="Zoidberg", owner=user)
return [pet1, pet2]
client = TestClient(app)
def test_filter_top_level_model():
response = client.post(
"/users", json={"email": "[email protected]", "password": "secret"}
)
assert response.json() == {"email": "[email protected]"}
def test_filter_second_level_model():
response = client.get("/pets/1")
assert response.json() == {
"name": "Nibbler",
"owner": {"email": "[email protected]"},
}
def test_list_of_models():
response = client.get("/pets/")
assert response.json() == [
{"name": "Nibbler", "owner": {"email": "[email protected]"}},
{"name": "Zoidberg", "owner": {"email": "[email protected]"}},
]

113
tests/test_schema_extra_examples.py

@ -1,8 +1,10 @@
from typing import Union from typing import Union
from dirty_equals import IsDict
from fastapi import Body, Cookie, FastAPI, Header, Path, Query from fastapi import Body, Cookie, FastAPI, Header, Path, Query
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel from pydantic import BaseModel, ConfigDict
app = FastAPI() app = FastAPI()
@ -10,8 +12,14 @@ app = FastAPI()
class Item(BaseModel): class Item(BaseModel):
data: str data: str
class Config: if PYDANTIC_V2:
schema_extra = {"example": {"data": "Data in schema_extra"}} model_config = ConfigDict(
json_schema_extra={"example": {"data": "Data in schema_extra"}}
)
else:
class Config:
schema_extra = {"example": {"data": "Data in schema_extra"}}
@app.post("/schema_extra/") @app.post("/schema_extra/")
@ -537,7 +545,16 @@ def test_openapi_schema():
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Data", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Data",
}
)
| IsDict(
# TODO: Remove this when deprecating Pydantic v1
{"title": "Data", "type": "string"}
),
"example": "query1", "example": "query1",
"name": "data", "name": "data",
"in": "query", "in": "query",
@ -568,7 +585,16 @@ def test_openapi_schema():
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Data", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Data",
}
)
| IsDict(
# TODO: Remove this when deprecating Pydantic v1
{"title": "Data", "type": "string"}
),
"examples": { "examples": {
"example1": { "example1": {
"summary": "Query example 1", "summary": "Query example 1",
@ -605,7 +631,16 @@ def test_openapi_schema():
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Data", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Data",
}
)
| IsDict(
# TODO: Remove this when deprecating Pydantic v1
{"title": "Data", "type": "string"}
),
"examples": { "examples": {
"example1": { "example1": {
"summary": "Query example 1", "summary": "Query example 1",
@ -642,7 +677,16 @@ def test_openapi_schema():
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Data", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Data",
}
)
| IsDict(
# TODO: Remove this when deprecating Pydantic v1
{"title": "Data", "type": "string"}
),
"example": "header1", "example": "header1",
"name": "data", "name": "data",
"in": "header", "in": "header",
@ -673,7 +717,16 @@ def test_openapi_schema():
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Data", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Data",
}
)
| IsDict(
# TODO: Remove this when deprecating Pydantic v1
{"title": "Data", "type": "string"}
),
"examples": { "examples": {
"example1": { "example1": {
"summary": "header example 1", "summary": "header example 1",
@ -710,7 +763,16 @@ def test_openapi_schema():
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Data", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Data",
}
)
| IsDict(
# TODO: Remove this when deprecating Pydantic v1
{"title": "Data", "type": "string"}
),
"examples": { "examples": {
"example1": { "example1": {
"summary": "Query example 1", "summary": "Query example 1",
@ -747,7 +809,16 @@ def test_openapi_schema():
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Data", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Data",
}
)
| IsDict(
# TODO: Remove this when deprecating Pydantic v1
{"title": "Data", "type": "string"}
),
"example": "cookie1", "example": "cookie1",
"name": "data", "name": "data",
"in": "cookie", "in": "cookie",
@ -778,7 +849,16 @@ def test_openapi_schema():
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Data", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Data",
}
)
| IsDict(
# TODO: Remove this when deprecating Pydantic v1
{"title": "Data", "type": "string"}
),
"examples": { "examples": {
"example1": { "example1": {
"summary": "cookie example 1", "summary": "cookie example 1",
@ -815,7 +895,16 @@ def test_openapi_schema():
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Data", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Data",
}
)
| IsDict(
# TODO: Remove this when deprecating Pydantic v1
{"title": "Data", "type": "string"}
),
"examples": { "examples": {
"example1": { "example1": {
"summary": "Query example 1", "summary": "Query example 1",

209
tests/test_security_oauth2.py

@ -1,7 +1,8 @@
import pytest from dirty_equals import IsDict
from fastapi import Depends, FastAPI, Security from fastapi import Depends, FastAPI, Security
from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from pydantic import BaseModel from pydantic import BaseModel
app = FastAPI() app = FastAPI()
@ -59,76 +60,136 @@ def test_security_oauth2_password_bearer_no_header():
assert response.json() == {"detail": "Not authenticated"} assert response.json() == {"detail": "Not authenticated"}
required_params = { def test_strict_login_no_data():
"detail": [ response = client.post("/login")
assert response.status_code == 422
assert response.json() == IsDict(
{ {
"loc": ["body", "grant_type"], "detail": [
"msg": "field required", {
"type": "value_error.missing", "type": "missing",
}, "loc": ["body", "grant_type"],
{ "msg": "Field required",
"loc": ["body", "username"], "input": None,
"msg": "field required", "url": match_pydantic_error_url("missing"),
"type": "value_error.missing", },
}, {
"type": "missing",
"loc": ["body", "username"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["body", "password"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{ {
"loc": ["body", "password"], "detail": [
"msg": "field required", {
"type": "value_error.missing", "loc": ["body", "grant_type"],
}, "msg": "field required",
] "type": "value_error.missing",
} },
{
"loc": ["body", "username"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "password"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
grant_type_required = {
"detail": [ def test_strict_login_no_grant_type():
response = client.post("/login", data={"username": "johndoe", "password": "secret"})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "grant_type"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{ {
"loc": ["body", "grant_type"], "detail": [
"msg": "field required", {
"type": "value_error.missing", "loc": ["body", "grant_type"],
"msg": "field required",
"type": "value_error.missing",
}
]
} }
] )
}
grant_type_incorrect = {
"detail": [ def test_strict_login_incorrect_grant_type():
response = client.post(
"/login",
data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
)
assert response.status_code == 422
assert response.json() == IsDict(
{ {
"loc": ["body", "grant_type"], "detail": [
"msg": 'string does not match regex "password"', {
"type": "value_error.str.regex", "type": "string_pattern_mismatch",
"ctx": {"pattern": "password"}, "loc": ["body", "grant_type"],
"msg": "String should match pattern 'password'",
"input": "incorrect",
"ctx": {"pattern": "password"},
"url": match_pydantic_error_url("string_pattern_mismatch"),
}
]
} }
] ) | IsDict(
} # TODO: remove when deprecating Pydantic v1
{
"detail": [
@pytest.mark.parametrize( {
"data,expected_status,expected_response", "loc": ["body", "grant_type"],
[ "msg": 'string does not match regex "password"',
(None, 422, required_params), "type": "value_error.str.regex",
({"username": "johndoe", "password": "secret"}, 422, grant_type_required), "ctx": {"pattern": "password"},
( }
{"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, ]
422, }
grant_type_incorrect, )
),
(
{"username": "johndoe", "password": "secret", "grant_type": "password"}, def test_strict_login_correct_grant_type():
200, response = client.post(
{ "/login",
"grant_type": "password", data={"username": "johndoe", "password": "secret", "grant_type": "password"},
"username": "johndoe", )
"password": "secret", assert response.status_code == 200
"scopes": [], assert response.json() == {
"client_id": None, "grant_type": "password",
"client_secret": None, "username": "johndoe",
}, "password": "secret",
), "scopes": [],
], "client_id": None,
) "client_secret": None,
def test_strict_login(data, expected_status, expected_response): }
response = client.post("/login", data=data)
assert response.status_code == expected_status
assert response.json() == expected_response
def test_openapi_schema(): def test_openapi_schema():
@ -199,8 +260,26 @@ def test_openapi_schema():
"username": {"title": "Username", "type": "string"}, "username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"}, "password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""}, "scope": {"title": "Scope", "type": "string", "default": ""},
"client_id": {"title": "Client Id", "type": "string"}, "client_id": IsDict(
"client_secret": {"title": "Client Secret", "type": "string"}, {
"title": "Client Id",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Client Id", "type": "string"}
),
"client_secret": IsDict(
{
"title": "Client Secret",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Client Secret", "type": "string"}
),
}, },
}, },
"ValidationError": { "ValidationError": {

209
tests/test_security_oauth2_optional.py

@ -1,9 +1,10 @@
from typing import Optional from typing import Optional
import pytest from dirty_equals import IsDict
from fastapi import Depends, FastAPI, Security from fastapi import Depends, FastAPI, Security
from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from pydantic import BaseModel from pydantic import BaseModel
app = FastAPI() app = FastAPI()
@ -63,76 +64,136 @@ def test_security_oauth2_password_bearer_no_header():
assert response.json() == {"msg": "Create an account first"} assert response.json() == {"msg": "Create an account first"}
required_params = { def test_strict_login_no_data():
"detail": [ response = client.post("/login")
assert response.status_code == 422
assert response.json() == IsDict(
{ {
"loc": ["body", "grant_type"], "detail": [
"msg": "field required", {
"type": "value_error.missing", "type": "missing",
}, "loc": ["body", "grant_type"],
{ "msg": "Field required",
"loc": ["body", "username"], "input": None,
"msg": "field required", "url": match_pydantic_error_url("missing"),
"type": "value_error.missing", },
}, {
"type": "missing",
"loc": ["body", "username"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["body", "password"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{ {
"loc": ["body", "password"], "detail": [
"msg": "field required", {
"type": "value_error.missing", "loc": ["body", "grant_type"],
}, "msg": "field required",
] "type": "value_error.missing",
} },
{
"loc": ["body", "username"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "password"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
grant_type_required = {
"detail": [ def test_strict_login_no_grant_type():
response = client.post("/login", data={"username": "johndoe", "password": "secret"})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "grant_type"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{ {
"loc": ["body", "grant_type"], "detail": [
"msg": "field required", {
"type": "value_error.missing", "loc": ["body", "grant_type"],
"msg": "field required",
"type": "value_error.missing",
}
]
} }
] )
}
grant_type_incorrect = {
"detail": [ def test_strict_login_incorrect_grant_type():
response = client.post(
"/login",
data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
)
assert response.status_code == 422
assert response.json() == IsDict(
{ {
"loc": ["body", "grant_type"], "detail": [
"msg": 'string does not match regex "password"', {
"type": "value_error.str.regex", "type": "string_pattern_mismatch",
"ctx": {"pattern": "password"}, "loc": ["body", "grant_type"],
"msg": "String should match pattern 'password'",
"input": "incorrect",
"ctx": {"pattern": "password"},
"url": match_pydantic_error_url("string_pattern_mismatch"),
}
]
} }
] ) | IsDict(
} # TODO: remove when deprecating Pydantic v1
{
"detail": [
@pytest.mark.parametrize( {
"data,expected_status,expected_response", "loc": ["body", "grant_type"],
[ "msg": 'string does not match regex "password"',
(None, 422, required_params), "type": "value_error.str.regex",
({"username": "johndoe", "password": "secret"}, 422, grant_type_required), "ctx": {"pattern": "password"},
( }
{"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, ]
422, }
grant_type_incorrect, )
),
(
{"username": "johndoe", "password": "secret", "grant_type": "password"}, def test_strict_login_correct_data():
200, response = client.post(
{ "/login",
"grant_type": "password", data={"username": "johndoe", "password": "secret", "grant_type": "password"},
"username": "johndoe", )
"password": "secret", assert response.status_code == 200
"scopes": [], assert response.json() == {
"client_id": None, "grant_type": "password",
"client_secret": None, "username": "johndoe",
}, "password": "secret",
), "scopes": [],
], "client_id": None,
) "client_secret": None,
def test_strict_login(data, expected_status, expected_response): }
response = client.post("/login", data=data)
assert response.status_code == expected_status
assert response.json() == expected_response
def test_openapi_schema(): def test_openapi_schema():
@ -203,8 +264,26 @@ def test_openapi_schema():
"username": {"title": "Username", "type": "string"}, "username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"}, "password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""}, "scope": {"title": "Scope", "type": "string", "default": ""},
"client_id": {"title": "Client Id", "type": "string"}, "client_id": IsDict(
"client_secret": {"title": "Client Secret", "type": "string"}, {
"title": "Client Id",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Client Id", "type": "string"}
),
"client_secret": IsDict(
{
"title": "Client Secret",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Client Secret", "type": "string"}
),
}, },
}, },
"ValidationError": { "ValidationError": {

209
tests/test_security_oauth2_optional_description.py

@ -1,9 +1,10 @@
from typing import Optional from typing import Optional
import pytest from dirty_equals import IsDict
from fastapi import Depends, FastAPI, Security from fastapi import Depends, FastAPI, Security
from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from pydantic import BaseModel from pydantic import BaseModel
app = FastAPI() app = FastAPI()
@ -64,76 +65,136 @@ def test_security_oauth2_password_bearer_no_header():
assert response.json() == {"msg": "Create an account first"} assert response.json() == {"msg": "Create an account first"}
required_params = { def test_strict_login_None():
"detail": [ response = client.post("/login", data=None)
assert response.status_code == 422
assert response.json() == IsDict(
{ {
"loc": ["body", "grant_type"], "detail": [
"msg": "field required", {
"type": "value_error.missing", "type": "missing",
}, "loc": ["body", "grant_type"],
{ "msg": "Field required",
"loc": ["body", "username"], "input": None,
"msg": "field required", "url": match_pydantic_error_url("missing"),
"type": "value_error.missing", },
}, {
"type": "missing",
"loc": ["body", "username"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["body", "password"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{ {
"loc": ["body", "password"], "detail": [
"msg": "field required", {
"type": "value_error.missing", "loc": ["body", "grant_type"],
}, "msg": "field required",
] "type": "value_error.missing",
} },
{
"loc": ["body", "username"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "password"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
grant_type_required = {
"detail": [ def test_strict_login_no_grant_type():
response = client.post("/login", data={"username": "johndoe", "password": "secret"})
assert response.status_code == 422
assert response.json() == IsDict(
{ {
"loc": ["body", "grant_type"], "detail": [
"msg": "field required", {
"type": "value_error.missing", "type": "missing",
"loc": ["body", "grant_type"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
} }
] ) | IsDict(
} # TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "grant_type"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
grant_type_incorrect = { def test_strict_login_incorrect_grant_type():
"detail": [ response = client.post(
"/login",
data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
)
assert response.status_code == 422
assert response.json() == IsDict(
{ {
"loc": ["body", "grant_type"], "detail": [
"msg": 'string does not match regex "password"', {
"type": "value_error.str.regex", "type": "string_pattern_mismatch",
"ctx": {"pattern": "password"}, "loc": ["body", "grant_type"],
"msg": "String should match pattern 'password'",
"input": "incorrect",
"ctx": {"pattern": "password"},
"url": match_pydantic_error_url("string_pattern_mismatch"),
}
]
} }
] ) | IsDict(
} # TODO: remove when deprecating Pydantic v1
{
"detail": [
@pytest.mark.parametrize( {
"data,expected_status,expected_response", "loc": ["body", "grant_type"],
[ "msg": 'string does not match regex "password"',
(None, 422, required_params), "type": "value_error.str.regex",
({"username": "johndoe", "password": "secret"}, 422, grant_type_required), "ctx": {"pattern": "password"},
( }
{"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, ]
422, }
grant_type_incorrect, )
),
(
{"username": "johndoe", "password": "secret", "grant_type": "password"}, def test_strict_login_correct_correct_grant_type():
200, response = client.post(
{ "/login",
"grant_type": "password", data={"username": "johndoe", "password": "secret", "grant_type": "password"},
"username": "johndoe", )
"password": "secret", assert response.status_code == 200, response.text
"scopes": [], assert response.json() == {
"client_id": None, "grant_type": "password",
"client_secret": None, "username": "johndoe",
}, "password": "secret",
), "scopes": [],
], "client_id": None,
) "client_secret": None,
def test_strict_login(data, expected_status, expected_response): }
response = client.post("/login", data=data)
assert response.status_code == expected_status
assert response.json() == expected_response
def test_openapi_schema(): def test_openapi_schema():
@ -204,8 +265,26 @@ def test_openapi_schema():
"username": {"title": "Username", "type": "string"}, "username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"}, "password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""}, "scope": {"title": "Scope", "type": "string", "default": ""},
"client_id": {"title": "Client Id", "type": "string"}, "client_id": IsDict(
"client_secret": {"title": "Client Secret", "type": "string"}, {
"title": "Client Id",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Client Id", "type": "string"}
),
"client_secret": IsDict(
{
"title": "Client Secret",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Client Secret", "type": "string"}
),
}, },
}, },
"ValidationError": { "ValidationError": {

2
tests/test_skip_defaults.py

@ -12,7 +12,7 @@ class SubModel(BaseModel):
class Model(BaseModel): class Model(BaseModel):
x: Optional[int] x: Optional[int] = None
sub: SubModel sub: SubModel

43
tests/test_sub_callbacks.py

@ -1,5 +1,6 @@
from typing import Optional from typing import Optional
from dirty_equals import IsDict
from fastapi import APIRouter, FastAPI from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel, HttpUrl from pydantic import BaseModel, HttpUrl
@ -98,13 +99,30 @@ def test_openapi_schema():
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": { "schema": IsDict(
"title": "Callback Url", {
"maxLength": 2083, "title": "Callback Url",
"minLength": 1, "anyOf": [
"type": "string", {
"format": "uri", "type": "string",
}, "format": "uri",
"minLength": 1,
"maxLength": 2083,
},
{"type": "null"},
],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "Callback Url",
"maxLength": 2083,
"minLength": 1,
"type": "string",
"format": "uri",
}
),
"name": "callback_url", "name": "callback_url",
"in": "query", "in": "query",
} }
@ -244,7 +262,16 @@ def test_openapi_schema():
"type": "object", "type": "object",
"properties": { "properties": {
"id": {"title": "Id", "type": "string"}, "id": {"title": "Id", "type": "string"},
"title": {"title": "Title", "type": "string"}, "title": IsDict(
{
"title": "Title",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Title", "type": "string"}
),
"customer": {"title": "Customer", "type": "string"}, "customer": {"title": "Customer", "type": "string"},
"total": {"title": "Total", "type": "number"}, "total": {"title": "Total", "type": "number"},
}, },

91
tests/test_tuples.py

@ -1,5 +1,6 @@
from typing import List, Tuple from typing import List, Tuple
from dirty_equals import IsDict
from fastapi import FastAPI, Form from fastapi import FastAPI, Form
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel from pydantic import BaseModel
@ -126,16 +127,31 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": IsDict(
"title": "Square", {
"maxItems": 2, "title": "Square",
"minItems": 2, "maxItems": 2,
"type": "array", "minItems": 2,
"items": [ "type": "array",
{"$ref": "#/components/schemas/Coordinate"}, "prefixItems": [
{"$ref": "#/components/schemas/Coordinate"}, {"$ref": "#/components/schemas/Coordinate"},
], {"$ref": "#/components/schemas/Coordinate"},
} ],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "Square",
"maxItems": 2,
"minItems": 2,
"type": "array",
"items": [
{"$ref": "#/components/schemas/Coordinate"},
{"$ref": "#/components/schemas/Coordinate"},
],
}
)
} }
}, },
"required": True, "required": True,
@ -198,13 +214,28 @@ def test_openapi_schema():
"required": ["values"], "required": ["values"],
"type": "object", "type": "object",
"properties": { "properties": {
"values": { "values": IsDict(
"title": "Values", {
"maxItems": 2, "title": "Values",
"minItems": 2, "maxItems": 2,
"type": "array", "minItems": 2,
"items": [{"type": "integer"}, {"type": "integer"}], "type": "array",
} "prefixItems": [
{"type": "integer"},
{"type": "integer"},
],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "Values",
"maxItems": 2,
"minItems": 2,
"type": "array",
"items": [{"type": "integer"}, {"type": "integer"}],
}
)
}, },
}, },
"Coordinate": { "Coordinate": {
@ -235,12 +266,26 @@ def test_openapi_schema():
"items": { "items": {
"title": "Items", "title": "Items",
"type": "array", "type": "array",
"items": { "items": IsDict(
"maxItems": 2, {
"minItems": 2, "maxItems": 2,
"type": "array", "minItems": 2,
"items": [{"type": "string"}, {"type": "string"}], "type": "array",
}, "prefixItems": [
{"type": "string"},
{"type": "string"},
],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"maxItems": 2,
"minItems": 2,
"type": "array",
"items": [{"type": "string"}, {"type": "string"}],
}
),
} }
}, },
}, },

12
tests/test_tutorial/test_additional_responses/test_tutorial002.py

@ -1,6 +1,7 @@
import os import os
import shutil import shutil
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from docs_src.additional_responses.tutorial002 import app from docs_src.additional_responses.tutorial002 import app
@ -64,7 +65,16 @@ def test_openapi_schema():
}, },
{ {
"required": False, "required": False,
"schema": {"title": "Img", "type": "boolean"}, "schema": IsDict(
{
"anyOf": [{"type": "boolean"}, {"type": "null"}],
"title": "Img",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Img", "type": "boolean"}
),
"name": "img", "name": "img",
"in": "query", "in": "query",
}, },

12
tests/test_tutorial/test_additional_responses/test_tutorial004.py

@ -1,6 +1,7 @@
import os import os
import shutil import shutil
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from docs_src.additional_responses.tutorial004 import app from docs_src.additional_responses.tutorial004 import app
@ -67,7 +68,16 @@ def test_openapi_schema():
}, },
{ {
"required": False, "required": False,
"schema": {"title": "Img", "type": "boolean"}, "schema": IsDict(
{
"anyOf": [{"type": "boolean"}, {"type": "null"}],
"title": "Img",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Img", "type": "boolean"}
),
"name": "img", "name": "img",
"in": "query", "in": "query",
}, },

4
tests/test_tutorial/test_async_sql_databases/test_tutorial001.py

@ -2,7 +2,11 @@ from fastapi.testclient import TestClient
from docs_src.async_sql_databases.tutorial001 import app from docs_src.async_sql_databases.tutorial001 import app
from ...utils import needs_pydanticv1
# TODO: pv2 add version with Pydantic v2
@needs_pydanticv1
def test_create_read(): def test_create_read():
with TestClient(app) as client: with TestClient(app) as client:
note = {"text": "Foo bar", "completed": False} note = {"text": "Foo bar", "completed": False}

18
tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py

@ -1,3 +1,4 @@
from dirty_equals import IsOneOf
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from docs_src.behind_a_proxy.tutorial003 import app from docs_src.behind_a_proxy.tutorial003 import app
@ -11,7 +12,7 @@ def test_main():
assert response.json() == {"message": "Hello World", "root_path": "/api/v1"} assert response.json() == {"message": "Hello World", "root_path": "/api/v1"}
def test_openapi(): def test_openapi_schema():
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == { assert response.json() == {
@ -19,9 +20,20 @@ def test_openapi():
"info": {"title": "FastAPI", "version": "0.1.0"}, "info": {"title": "FastAPI", "version": "0.1.0"},
"servers": [ "servers": [
{"url": "/api/v1"}, {"url": "/api/v1"},
{"url": "https://stag.example.com", "description": "Staging environment"},
{ {
"url": "https://prod.example.com", "url": IsOneOf(
"https://stag.example.com/",
# TODO: remove when deprecating Pydantic v1
"https://stag.example.com",
),
"description": "Staging environment",
},
{
"url": IsOneOf(
"https://prod.example.com/",
# TODO: remove when deprecating Pydantic v1
"https://prod.example.com",
),
"description": "Production environment", "description": "Production environment",
}, },
], ],

18
tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py

@ -1,3 +1,4 @@
from dirty_equals import IsOneOf
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from docs_src.behind_a_proxy.tutorial004 import app from docs_src.behind_a_proxy.tutorial004 import app
@ -11,16 +12,27 @@ def test_main():
assert response.json() == {"message": "Hello World", "root_path": "/api/v1"} assert response.json() == {"message": "Hello World", "root_path": "/api/v1"}
def test_openapi(): def test_openapi_schema():
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == { assert response.json() == {
"openapi": "3.0.2", "openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"}, "info": {"title": "FastAPI", "version": "0.1.0"},
"servers": [ "servers": [
{"url": "https://stag.example.com", "description": "Staging environment"},
{ {
"url": "https://prod.example.com", "url": IsOneOf(
"https://stag.example.com/",
# TODO: remove when deprecating Pydantic v1
"https://stag.example.com",
),
"description": "Staging environment",
},
{
"url": IsOneOf(
"https://prod.example.com/",
# TODO: remove when deprecating Pydantic v1
"https://prod.example.com",
),
"description": "Production environment", "description": "Production environment",
}, },
], ],

476
tests/test_tutorial/test_bigger_applications/test_main.py

@ -1,138 +1,368 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from docs_src.bigger_applications.app.main import app
client = TestClient(app) @pytest.fixture(name="client")
def get_client():
from docs_src.bigger_applications.app.main import app
client = TestClient(app)
return client
no_jessica = {
"detail": [ def test_users_token_jessica(client: TestClient):
response = client.get("/users?token=jessica")
assert response.status_code == 200
assert response.json() == [{"username": "Rick"}, {"username": "Morty"}]
def test_users_with_no_token(client: TestClient):
response = client.get("/users")
assert response.status_code == 422
assert response.json() == IsDict(
{ {
"loc": ["query", "token"], "detail": [
"msg": "field required", {
"type": "value_error.missing", "type": "missing",
}, "loc": ["query", "token"],
] "msg": "Field required",
} "input": None,
"url": match_pydantic_error_url("missing"),
}
@pytest.mark.parametrize( ]
"path,expected_status,expected_response,headers", }
[ ) | IsDict(
( # TODO: remove when deprecating Pydantic v1
"/users?token=jessica", {
200, "detail": [
[{"username": "Rick"}, {"username": "Morty"}], {
{}, "loc": ["query", "token"],
), "msg": "field required",
("/users", 422, no_jessica, {}), "type": "value_error.missing",
("/users/foo?token=jessica", 200, {"username": "foo"}, {}), },
("/users/foo", 422, no_jessica, {}), ]
("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}), }
("/users/me", 422, no_jessica, {}), )
(
"/users?token=monica",
400, def test_users_foo_token_jessica(client: TestClient):
{"detail": "No Jessica token provided"}, response = client.get("/users/foo?token=jessica")
{}, assert response.status_code == 200
), assert response.json() == {"username": "foo"}
(
"/items?token=jessica",
200, def test_users_foo_with_no_token(client: TestClient):
{"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}, response = client.get("/users/foo")
{"X-Token": "fake-super-secret-token"}, assert response.status_code == 422
), assert response.json() == IsDict(
("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), {
( "detail": [
"/items/plumbus?token=jessica", {
200, "type": "missing",
{"name": "Plumbus", "item_id": "plumbus"}, "loc": ["query", "token"],
{"X-Token": "fake-super-secret-token"}, "msg": "Field required",
), "input": None,
( "url": match_pydantic_error_url("missing"),
"/items/bar?token=jessica", }
404, ]
{"detail": "Item not found"}, }
{"X-Token": "fake-super-secret-token"}, ) | IsDict(
), # TODO: remove when deprecating Pydantic v1
("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), {
( "detail": [
"/items?token=jessica", {
400, "loc": ["query", "token"],
{"detail": "X-Token header invalid"}, "msg": "field required",
{"X-Token": "invalid"}, "type": "value_error.missing",
), },
( ]
"/items/bar?token=jessica", }
400, )
{"detail": "X-Token header invalid"},
{"X-Token": "invalid"},
), def test_users_me_token_jessica(client: TestClient):
( response = client.get("/users/me?token=jessica")
"/items?token=jessica", assert response.status_code == 200
422, assert response.json() == {"username": "fakecurrentuser"}
{
"detail": [
{ def test_users_me_with_no_token(client: TestClient):
"loc": ["header", "x-token"], response = client.get("/users/me")
"msg": "field required", assert response.status_code == 422
"type": "value_error.missing", assert response.json() == IsDict(
} {
] "detail": [
}, {
{}, "type": "missing",
), "loc": ["query", "token"],
( "msg": "Field required",
"/items/plumbus?token=jessica", "input": None,
422, "url": match_pydantic_error_url("missing"),
{ }
"detail": [ ]
{ }
"loc": ["header", "x-token"], ) | IsDict(
"msg": "field required", # TODO: remove when deprecating Pydantic v1
"type": "value_error.missing", {
} "detail": [
] {
}, "loc": ["query", "token"],
{}, "msg": "field required",
), "type": "value_error.missing",
("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}), },
("/", 422, no_jessica, {}), ]
], }
) )
def test_get_path(path, expected_status, expected_response, headers):
response = client.get(path, headers=headers)
assert response.status_code == expected_status def test_users_token_monica_with_no_jessica(client: TestClient):
assert response.json() == expected_response response = client.get("/users?token=monica")
assert response.status_code == 400
assert response.json() == {"detail": "No Jessica token provided"}
def test_put_no_header():
response = client.put("/items/foo")
assert response.status_code == 422, response.text def test_items_token_jessica(client: TestClient):
response = client.get(
"/items?token=jessica", headers={"X-Token": "fake-super-secret-token"}
)
assert response.status_code == 200
assert response.json() == { assert response.json() == {
"detail": [ "plumbus": {"name": "Plumbus"},
{ "gun": {"name": "Portal Gun"},
"loc": ["query", "token"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["header", "x-token"],
"msg": "field required",
"type": "value_error.missing",
},
]
} }
def test_put_invalid_header(): def test_items_with_no_token_jessica(client: TestClient):
response = client.get("/items", headers={"X-Token": "fake-super-secret-token"})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "token"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "token"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
def test_items_plumbus_token_jessica(client: TestClient):
response = client.get(
"/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"}
)
assert response.status_code == 200
assert response.json() == {"name": "Plumbus", "item_id": "plumbus"}
def test_items_bar_token_jessica(client: TestClient):
response = client.get(
"/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"}
)
assert response.status_code == 404
assert response.json() == {"detail": "Item not found"}
def test_items_plumbus_with_no_token(client: TestClient):
response = client.get(
"/items/plumbus", headers={"X-Token": "fake-super-secret-token"}
)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "token"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "token"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
def test_items_with_invalid_token(client: TestClient):
response = client.get("/items?token=jessica", headers={"X-Token": "invalid"})
assert response.status_code == 400
assert response.json() == {"detail": "X-Token header invalid"}
def test_items_bar_with_invalid_token(client: TestClient):
response = client.get("/items/bar?token=jessica", headers={"X-Token": "invalid"})
assert response.status_code == 400
assert response.json() == {"detail": "X-Token header invalid"}
def test_items_with_missing_x_token_header(client: TestClient):
response = client.get("/items?token=jessica")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["header", "x-token"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["header", "x-token"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_items_plumbus_with_missing_x_token_header(client: TestClient):
response = client.get("/items/plumbus?token=jessica")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["header", "x-token"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["header", "x-token"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_root_token_jessica(client: TestClient):
response = client.get("/?token=jessica")
assert response.status_code == 200
assert response.json() == {"message": "Hello Bigger Applications!"}
def test_root_with_no_token(client: TestClient):
response = client.get("/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "token"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "token"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
def test_put_no_header(client: TestClient):
response = client.put("/items/foo")
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "token"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["header", "x-token"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "token"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["header", "x-token"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
def test_put_invalid_header(client: TestClient):
response = client.put("/items/foo", headers={"X-Token": "invalid"}) response = client.put("/items/foo", headers={"X-Token": "invalid"})
assert response.status_code == 400, response.text assert response.status_code == 400, response.text
assert response.json() == {"detail": "X-Token header invalid"} assert response.json() == {"detail": "X-Token header invalid"}
def test_put(): def test_put(client: TestClient):
response = client.put( response = client.put(
"/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"}
) )
@ -140,7 +370,7 @@ def test_put():
assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"} assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"}
def test_put_forbidden(): def test_put_forbidden(client: TestClient):
response = client.put( response = client.put(
"/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"}
) )
@ -148,7 +378,7 @@ def test_put_forbidden():
assert response.json() == {"detail": "You can only update the item: plumbus"} assert response.json() == {"detail": "You can only update the item: plumbus"}
def test_admin(): def test_admin(client: TestClient):
response = client.post( response = client.post(
"/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"} "/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"}
) )
@ -156,13 +386,13 @@ def test_admin():
assert response.json() == {"message": "Admin getting schwifty"} assert response.json() == {"message": "Admin getting schwifty"}
def test_admin_invalid_header(): def test_admin_invalid_header(client: TestClient):
response = client.post("/admin/", headers={"X-Token": "invalid"}) response = client.post("/admin/", headers={"X-Token": "invalid"})
assert response.status_code == 400, response.text assert response.status_code == 400, response.text
assert response.json() == {"detail": "X-Token header invalid"} assert response.json() == {"detail": "X-Token header invalid"}
def test_openapi_schema(): def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
assert response.json() == { assert response.json() == {

476
tests/test_tutorial/test_bigger_applications/test_main_an.py

@ -1,138 +1,368 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from docs_src.bigger_applications.app_an.main import app
client = TestClient(app) @pytest.fixture(name="client")
def get_client():
from docs_src.bigger_applications.app_an.main import app
client = TestClient(app)
return client
no_jessica = {
"detail": [ def test_users_token_jessica(client: TestClient):
response = client.get("/users?token=jessica")
assert response.status_code == 200
assert response.json() == [{"username": "Rick"}, {"username": "Morty"}]
def test_users_with_no_token(client: TestClient):
response = client.get("/users")
assert response.status_code == 422
assert response.json() == IsDict(
{ {
"loc": ["query", "token"], "detail": [
"msg": "field required", {
"type": "value_error.missing", "type": "missing",
}, "loc": ["query", "token"],
] "msg": "Field required",
} "input": None,
"url": match_pydantic_error_url("missing"),
}
@pytest.mark.parametrize( ]
"path,expected_status,expected_response,headers", }
[ ) | IsDict(
( # TODO: remove when deprecating Pydantic v1
"/users?token=jessica", {
200, "detail": [
[{"username": "Rick"}, {"username": "Morty"}], {
{}, "loc": ["query", "token"],
), "msg": "field required",
("/users", 422, no_jessica, {}), "type": "value_error.missing",
("/users/foo?token=jessica", 200, {"username": "foo"}, {}), },
("/users/foo", 422, no_jessica, {}), ]
("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}), }
("/users/me", 422, no_jessica, {}), )
(
"/users?token=monica",
400, def test_users_foo_token_jessica(client: TestClient):
{"detail": "No Jessica token provided"}, response = client.get("/users/foo?token=jessica")
{}, assert response.status_code == 200
), assert response.json() == {"username": "foo"}
(
"/items?token=jessica",
200, def test_users_foo_with_no_token(client: TestClient):
{"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}, response = client.get("/users/foo")
{"X-Token": "fake-super-secret-token"}, assert response.status_code == 422
), assert response.json() == IsDict(
("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), {
( "detail": [
"/items/plumbus?token=jessica", {
200, "type": "missing",
{"name": "Plumbus", "item_id": "plumbus"}, "loc": ["query", "token"],
{"X-Token": "fake-super-secret-token"}, "msg": "Field required",
), "input": None,
( "url": match_pydantic_error_url("missing"),
"/items/bar?token=jessica", }
404, ]
{"detail": "Item not found"}, }
{"X-Token": "fake-super-secret-token"}, ) | IsDict(
), # TODO: remove when deprecating Pydantic v1
("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), {
( "detail": [
"/items?token=jessica", {
400, "loc": ["query", "token"],
{"detail": "X-Token header invalid"}, "msg": "field required",
{"X-Token": "invalid"}, "type": "value_error.missing",
), },
( ]
"/items/bar?token=jessica", }
400, )
{"detail": "X-Token header invalid"},
{"X-Token": "invalid"},
), def test_users_me_token_jessica(client: TestClient):
( response = client.get("/users/me?token=jessica")
"/items?token=jessica", assert response.status_code == 200
422, assert response.json() == {"username": "fakecurrentuser"}
{
"detail": [
{ def test_users_me_with_no_token(client: TestClient):
"loc": ["header", "x-token"], response = client.get("/users/me")
"msg": "field required", assert response.status_code == 422
"type": "value_error.missing", assert response.json() == IsDict(
} {
] "detail": [
}, {
{}, "type": "missing",
), "loc": ["query", "token"],
( "msg": "Field required",
"/items/plumbus?token=jessica", "input": None,
422, "url": match_pydantic_error_url("missing"),
{ }
"detail": [ ]
{ }
"loc": ["header", "x-token"], ) | IsDict(
"msg": "field required", # TODO: remove when deprecating Pydantic v1
"type": "value_error.missing", {
} "detail": [
] {
}, "loc": ["query", "token"],
{}, "msg": "field required",
), "type": "value_error.missing",
("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}), },
("/", 422, no_jessica, {}), ]
], }
) )
def test_get_path(path, expected_status, expected_response, headers):
response = client.get(path, headers=headers)
assert response.status_code == expected_status def test_users_token_monica_with_no_jessica(client: TestClient):
assert response.json() == expected_response response = client.get("/users?token=monica")
assert response.status_code == 400
assert response.json() == {"detail": "No Jessica token provided"}
def test_put_no_header():
response = client.put("/items/foo")
assert response.status_code == 422, response.text def test_items_token_jessica(client: TestClient):
response = client.get(
"/items?token=jessica", headers={"X-Token": "fake-super-secret-token"}
)
assert response.status_code == 200
assert response.json() == { assert response.json() == {
"detail": [ "plumbus": {"name": "Plumbus"},
{ "gun": {"name": "Portal Gun"},
"loc": ["query", "token"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["header", "x-token"],
"msg": "field required",
"type": "value_error.missing",
},
]
} }
def test_put_invalid_header(): def test_items_with_no_token_jessica(client: TestClient):
response = client.get("/items", headers={"X-Token": "fake-super-secret-token"})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "token"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "token"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
def test_items_plumbus_token_jessica(client: TestClient):
response = client.get(
"/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"}
)
assert response.status_code == 200
assert response.json() == {"name": "Plumbus", "item_id": "plumbus"}
def test_items_bar_token_jessica(client: TestClient):
response = client.get(
"/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"}
)
assert response.status_code == 404
assert response.json() == {"detail": "Item not found"}
def test_items_plumbus_with_no_token(client: TestClient):
response = client.get(
"/items/plumbus", headers={"X-Token": "fake-super-secret-token"}
)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "token"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "token"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
def test_items_with_invalid_token(client: TestClient):
response = client.get("/items?token=jessica", headers={"X-Token": "invalid"})
assert response.status_code == 400
assert response.json() == {"detail": "X-Token header invalid"}
def test_items_bar_with_invalid_token(client: TestClient):
response = client.get("/items/bar?token=jessica", headers={"X-Token": "invalid"})
assert response.status_code == 400
assert response.json() == {"detail": "X-Token header invalid"}
def test_items_with_missing_x_token_header(client: TestClient):
response = client.get("/items?token=jessica")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["header", "x-token"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["header", "x-token"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_items_plumbus_with_missing_x_token_header(client: TestClient):
response = client.get("/items/plumbus?token=jessica")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["header", "x-token"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["header", "x-token"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_root_token_jessica(client: TestClient):
response = client.get("/?token=jessica")
assert response.status_code == 200
assert response.json() == {"message": "Hello Bigger Applications!"}
def test_root_with_no_token(client: TestClient):
response = client.get("/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "token"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "token"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
def test_put_no_header(client: TestClient):
response = client.put("/items/foo")
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "token"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["header", "x-token"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "token"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["header", "x-token"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
def test_put_invalid_header(client: TestClient):
response = client.put("/items/foo", headers={"X-Token": "invalid"}) response = client.put("/items/foo", headers={"X-Token": "invalid"})
assert response.status_code == 400, response.text assert response.status_code == 400, response.text
assert response.json() == {"detail": "X-Token header invalid"} assert response.json() == {"detail": "X-Token header invalid"}
def test_put(): def test_put(client: TestClient):
response = client.put( response = client.put(
"/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"}
) )
@ -140,7 +370,7 @@ def test_put():
assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"} assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"}
def test_put_forbidden(): def test_put_forbidden(client: TestClient):
response = client.put( response = client.put(
"/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"}
) )
@ -148,7 +378,7 @@ def test_put_forbidden():
assert response.json() == {"detail": "You can only update the item: plumbus"} assert response.json() == {"detail": "You can only update the item: plumbus"}
def test_admin(): def test_admin(client: TestClient):
response = client.post( response = client.post(
"/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"} "/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"}
) )
@ -156,13 +386,13 @@ def test_admin():
assert response.json() == {"message": "Admin getting schwifty"} assert response.json() == {"message": "Admin getting schwifty"}
def test_admin_invalid_header(): def test_admin_invalid_header(client: TestClient):
response = client.post("/admin/", headers={"X-Token": "invalid"}) response = client.post("/admin/", headers={"X-Token": "invalid"})
assert response.status_code == 400, response.text assert response.status_code == 400, response.text
assert response.json() == {"detail": "X-Token header invalid"} assert response.json() == {"detail": "X-Token header invalid"}
def test_openapi_schema(): def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
assert response.json() == { assert response.json() == {

470
tests/test_tutorial/test_bigger_applications/test_main_an_py39.py

@ -1,18 +1,10 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py39 from ...utils import needs_py39
no_jessica = {
"detail": [
{
"loc": ["query", "token"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
@pytest.fixture(name="client") @pytest.fixture(name="client")
def get_client(): def get_client():
@ -23,116 +15,366 @@ def get_client():
@needs_py39 @needs_py39
@pytest.mark.parametrize( def test_users_token_jessica(client: TestClient):
"path,expected_status,expected_response,headers", response = client.get("/users?token=jessica")
[ assert response.status_code == 200
( assert response.json() == [{"username": "Rick"}, {"username": "Morty"}]
"/users?token=jessica",
200,
[{"username": "Rick"}, {"username": "Morty"}], @needs_py39
{}, def test_users_with_no_token(client: TestClient):
), response = client.get("/users")
("/users", 422, no_jessica, {}), assert response.status_code == 422
("/users/foo?token=jessica", 200, {"username": "foo"}, {}), assert response.json() == IsDict(
("/users/foo", 422, no_jessica, {}), {
("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}), "detail": [
("/users/me", 422, no_jessica, {}), {
( "type": "missing",
"/users?token=monica", "loc": ["query", "token"],
400, "msg": "Field required",
{"detail": "No Jessica token provided"}, "input": None,
{}, "url": match_pydantic_error_url("missing"),
), }
( ]
"/items?token=jessica", }
200, ) | IsDict(
{"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}, # TODO: remove when deprecating Pydantic v1
{"X-Token": "fake-super-secret-token"}, {
), "detail": [
("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), {
( "loc": ["query", "token"],
"/items/plumbus?token=jessica", "msg": "field required",
200, "type": "value_error.missing",
{"name": "Plumbus", "item_id": "plumbus"}, },
{"X-Token": "fake-super-secret-token"}, ]
), }
( )
"/items/bar?token=jessica",
404,
{"detail": "Item not found"}, @needs_py39
{"X-Token": "fake-super-secret-token"}, def test_users_foo_token_jessica(client: TestClient):
), response = client.get("/users/foo?token=jessica")
("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), assert response.status_code == 200
( assert response.json() == {"username": "foo"}
"/items?token=jessica",
400,
{"detail": "X-Token header invalid"}, @needs_py39
{"X-Token": "invalid"}, def test_users_foo_with_no_token(client: TestClient):
), response = client.get("/users/foo")
( assert response.status_code == 422
"/items/bar?token=jessica", assert response.json() == IsDict(
400, {
{"detail": "X-Token header invalid"}, "detail": [
{"X-Token": "invalid"}, {
), "type": "missing",
( "loc": ["query", "token"],
"/items?token=jessica", "msg": "Field required",
422, "input": None,
{ "url": match_pydantic_error_url("missing"),
"detail": [ }
{ ]
"loc": ["header", "x-token"], }
"msg": "field required", ) | IsDict(
"type": "value_error.missing", # TODO: remove when deprecating Pydantic v1
} {
] "detail": [
}, {
{}, "loc": ["query", "token"],
), "msg": "field required",
( "type": "value_error.missing",
"/items/plumbus?token=jessica", },
422, ]
{ }
"detail": [ )
{
"loc": ["header", "x-token"],
"msg": "field required", @needs_py39
"type": "value_error.missing", def test_users_me_token_jessica(client: TestClient):
} response = client.get("/users/me?token=jessica")
] assert response.status_code == 200
}, assert response.json() == {"username": "fakecurrentuser"}
{},
),
("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}), @needs_py39
("/", 422, no_jessica, {}), def test_users_me_with_no_token(client: TestClient):
], response = client.get("/users/me")
) assert response.status_code == 422
def test_get_path( assert response.json() == IsDict(
path, expected_status, expected_response, headers, client: TestClient {
): "detail": [
response = client.get(path, headers=headers) {
assert response.status_code == expected_status "type": "missing",
assert response.json() == expected_response "loc": ["query", "token"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "token"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
@needs_py39
def test_users_token_monica_with_no_jessica(client: TestClient):
response = client.get("/users?token=monica")
assert response.status_code == 400
assert response.json() == {"detail": "No Jessica token provided"}
@needs_py39
def test_items_token_jessica(client: TestClient):
response = client.get(
"/items?token=jessica", headers={"X-Token": "fake-super-secret-token"}
)
assert response.status_code == 200
assert response.json() == {
"plumbus": {"name": "Plumbus"},
"gun": {"name": "Portal Gun"},
}
@needs_py39
def test_items_with_no_token_jessica(client: TestClient):
response = client.get("/items", headers={"X-Token": "fake-super-secret-token"})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "token"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "token"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
@needs_py39
def test_items_plumbus_token_jessica(client: TestClient):
response = client.get(
"/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"}
)
assert response.status_code == 200
assert response.json() == {"name": "Plumbus", "item_id": "plumbus"}
@needs_py39
def test_items_bar_token_jessica(client: TestClient):
response = client.get(
"/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"}
)
assert response.status_code == 404
assert response.json() == {"detail": "Item not found"}
@needs_py39
def test_items_plumbus_with_no_token(client: TestClient):
response = client.get(
"/items/plumbus", headers={"X-Token": "fake-super-secret-token"}
)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "token"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "token"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
@needs_py39
def test_items_with_invalid_token(client: TestClient):
response = client.get("/items?token=jessica", headers={"X-Token": "invalid"})
assert response.status_code == 400
assert response.json() == {"detail": "X-Token header invalid"}
@needs_py39
def test_items_bar_with_invalid_token(client: TestClient):
response = client.get("/items/bar?token=jessica", headers={"X-Token": "invalid"})
assert response.status_code == 400
assert response.json() == {"detail": "X-Token header invalid"}
@needs_py39
def test_items_with_missing_x_token_header(client: TestClient):
response = client.get("/items?token=jessica")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["header", "x-token"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["header", "x-token"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@needs_py39
def test_items_plumbus_with_missing_x_token_header(client: TestClient):
response = client.get("/items/plumbus?token=jessica")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["header", "x-token"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["header", "x-token"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@needs_py39
def test_root_token_jessica(client: TestClient):
response = client.get("/?token=jessica")
assert response.status_code == 200
assert response.json() == {"message": "Hello Bigger Applications!"}
@needs_py39
def test_root_with_no_token(client: TestClient):
response = client.get("/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "token"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "token"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
@needs_py39 @needs_py39
def test_put_no_header(client: TestClient): def test_put_no_header(client: TestClient):
response = client.put("/items/foo") response = client.put("/items/foo")
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
assert response.json() == { assert response.json() == IsDict(
"detail": [ {
{ "detail": [
"loc": ["query", "token"], {
"msg": "field required", "type": "missing",
"type": "value_error.missing", "loc": ["query", "token"],
}, "msg": "Field required",
{ "input": None,
"loc": ["header", "x-token"], "url": match_pydantic_error_url("missing"),
"msg": "field required", },
"type": "value_error.missing", {
}, "type": "missing",
] "loc": ["header", "x-token"],
} "msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "token"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["header", "x-token"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
@needs_py39 @needs_py39

459
tests/test_tutorial/test_body/test_tutorial001.py

@ -1,134 +1,268 @@
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from docs_src.body.tutorial001 import app
client = TestClient(app) @pytest.fixture
def client():
from docs_src.body.tutorial001 import app
client = TestClient(app)
return client
price_missing = {
"detail": [ def test_body_float(client: TestClient):
response = client.post("/items/", json={"name": "Foo", "price": 50.5})
assert response.status_code == 200
assert response.json() == {
"name": "Foo",
"price": 50.5,
"description": None,
"tax": None,
}
def test_post_with_str_float(client: TestClient):
response = client.post("/items/", json={"name": "Foo", "price": "50.5"})
assert response.status_code == 200
assert response.json() == {
"name": "Foo",
"price": 50.5,
"description": None,
"tax": None,
}
def test_post_with_str_float_description(client: TestClient):
response = client.post(
"/items/", json={"name": "Foo", "price": "50.5", "description": "Some Foo"}
)
assert response.status_code == 200
assert response.json() == {
"name": "Foo",
"price": 50.5,
"description": "Some Foo",
"tax": None,
}
def test_post_with_str_float_description_tax(client: TestClient):
response = client.post(
"/items/",
json={"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3},
)
assert response.status_code == 200
assert response.json() == {
"name": "Foo",
"price": 50.5,
"description": "Some Foo",
"tax": 0.3,
}
def test_post_with_only_name(client: TestClient):
response = client.post("/items/", json={"name": "Foo"})
assert response.status_code == 422
assert response.json() == IsDict(
{ {
"loc": ["body", "price"], "detail": [
"msg": "field required", {
"type": "value_error.missing", "type": "missing",
"loc": ["body", "price"],
"msg": "Field required",
"input": {"name": "Foo"},
"url": match_pydantic_error_url("missing"),
}
]
} }
] ) | IsDict(
} # TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "price"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
price_not_float = { def test_post_with_only_name_price(client: TestClient):
"detail": [ response = client.post("/items/", json={"name": "Foo", "price": "twenty"})
assert response.status_code == 422
assert response.json() == IsDict(
{ {
"loc": ["body", "price"], "detail": [
"msg": "value is not a valid float", {
"type": "type_error.float", "type": "float_parsing",
"loc": ["body", "price"],
"msg": "Input should be a valid number, unable to parse string as an number",
"input": "twenty",
"url": match_pydantic_error_url("float_parsing"),
}
]
} }
] ) | IsDict(
} # TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "price"],
"msg": "value is not a valid float",
"type": "type_error.float",
}
]
}
)
name_price_missing = {
"detail": [ def test_post_with_no_data(client: TestClient):
response = client.post("/items/", json={})
assert response.status_code == 422
assert response.json() == IsDict(
{ {
"loc": ["body", "name"], "detail": [
"msg": "field required", {
"type": "value_error.missing", "type": "missing",
}, "loc": ["body", "name"],
"msg": "Field required",
"input": {},
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["body", "price"],
"msg": "Field required",
"input": {},
"url": match_pydantic_error_url("missing"),
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{ {
"loc": ["body", "price"], "detail": [
"msg": "field required", {
"type": "value_error.missing", "loc": ["body", "name"],
}, "msg": "field required",
] "type": "value_error.missing",
} },
{
body_missing = { "loc": ["body", "price"],
"detail": [ "msg": "field required",
{"loc": ["body"], "msg": "field required", "type": "value_error.missing"} "type": "value_error.missing",
] },
} ]
}
)
@pytest.mark.parametrize(
"path,body,expected_status,expected_response",
[ def test_post_with_none(client: TestClient):
( response = client.post("/items/", json=None)
"/items/", assert response.status_code == 422
{"name": "Foo", "price": 50.5}, assert response.json() == IsDict(
200, {
{"name": "Foo", "price": 50.5, "description": None, "tax": None}, "detail": [
), {
( "type": "missing",
"/items/", "loc": ["body"],
{"name": "Foo", "price": "50.5"}, "msg": "Field required",
200, "input": None,
{"name": "Foo", "price": 50.5, "description": None, "tax": None}, "url": match_pydantic_error_url("missing"),
), }
( ]
"/items/", }
{"name": "Foo", "price": "50.5", "description": "Some Foo"}, ) | IsDict(
200, # TODO: remove when deprecating Pydantic v1
{"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": None}, {
), "detail": [
( {
"/items/", "loc": ["body"],
{"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3}, "msg": "field required",
200, "type": "value_error.missing",
{"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": 0.3}, }
), ]
("/items/", {"name": "Foo"}, 422, price_missing), }
("/items/", {"name": "Foo", "price": "twenty"}, 422, price_not_float), )
("/items/", {}, 422, name_price_missing),
("/items/", None, 422, body_missing),
], def test_post_broken_body(client: TestClient):
)
def test_post_body(path, body, expected_status, expected_response):
response = client.post(path, json=body)
assert response.status_code == expected_status
assert response.json() == expected_response
def test_post_broken_body():
response = client.post( response = client.post(
"/items/", "/items/",
headers={"content-type": "application/json"}, headers={"content-type": "application/json"},
content="{some broken json}", content="{some broken json}",
) )
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
assert response.json() == { assert response.json() == IsDict(
"detail": [ {
{ "detail": [
"loc": ["body", 1], {
"msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", "type": "json_invalid",
"type": "value_error.jsondecode", "loc": ["body", 1],
"ctx": { "msg": "JSON decode error",
"msg": "Expecting property name enclosed in double quotes", "input": {},
"doc": "{some broken json}", "ctx": {
"pos": 1, "error": "Expecting property name enclosed in double quotes"
"lineno": 1, },
"colno": 2, }
}, ]
} }
] ) | IsDict(
} # TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", 1],
"msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)",
"type": "value_error.jsondecode",
"ctx": {
"msg": "Expecting property name enclosed in double quotes",
"doc": "{some broken json}",
"pos": 1,
"lineno": 1,
"colno": 2,
},
}
]
}
)
def test_post_form_for_json(): def test_post_form_for_json(client: TestClient):
response = client.post("/items/", data={"name": "Foo", "price": 50.5}) response = client.post("/items/", data={"name": "Foo", "price": 50.5})
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
assert response.json() == { assert response.json() == IsDict(
"detail": [ {
{ "detail": [
"loc": ["body"], {
"msg": "value is not a valid dict", "type": "dict_attributes_type",
"type": "type_error.dict", "loc": ["body"],
} "msg": "Input should be a valid dictionary or instance to extract fields from",
] "input": "name=Foo&price=50.5",
} "url": match_pydantic_error_url("dict_attributes_type"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body"],
"msg": "value is not a valid dict",
"type": "type_error.dict",
}
]
}
)
def test_explicit_content_type(): def test_explicit_content_type(client: TestClient):
response = client.post( response = client.post(
"/items/", "/items/",
content='{"name": "Foo", "price": 50.5}', content='{"name": "Foo", "price": 50.5}',
@ -137,7 +271,7 @@ def test_explicit_content_type():
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
def test_geo_json(): def test_geo_json(client: TestClient):
response = client.post( response = client.post(
"/items/", "/items/",
content='{"name": "Foo", "price": 50.5}', content='{"name": "Foo", "price": 50.5}',
@ -146,7 +280,7 @@ def test_geo_json():
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
def test_no_content_type_is_json(): def test_no_content_type_is_json(client: TestClient):
response = client.post( response = client.post(
"/items/", "/items/",
content='{"name": "Foo", "price": 50.5}', content='{"name": "Foo", "price": 50.5}',
@ -160,43 +294,104 @@ def test_no_content_type_is_json():
} }
def test_wrong_headers(): def test_wrong_headers(client: TestClient):
data = '{"name": "Foo", "price": 50.5}' data = '{"name": "Foo", "price": 50.5}'
invalid_dict = {
"detail": [
{
"loc": ["body"],
"msg": "value is not a valid dict",
"type": "type_error.dict",
}
]
}
response = client.post( response = client.post(
"/items/", content=data, headers={"Content-Type": "text/plain"} "/items/", content=data, headers={"Content-Type": "text/plain"}
) )
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
assert response.json() == invalid_dict assert response.json() == IsDict(
{
"detail": [
{
"type": "dict_attributes_type",
"loc": ["body"],
"msg": "Input should be a valid dictionary or instance to extract fields from",
"input": '{"name": "Foo", "price": 50.5}',
"url": match_pydantic_error_url(
"dict_attributes_type"
), # "https://errors.pydantic.dev/0.38.0/v/dict_attributes_type",
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body"],
"msg": "value is not a valid dict",
"type": "type_error.dict",
}
]
}
)
response = client.post( response = client.post(
"/items/", content=data, headers={"Content-Type": "application/geo+json-seq"} "/items/", content=data, headers={"Content-Type": "application/geo+json-seq"}
) )
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
assert response.json() == invalid_dict assert response.json() == IsDict(
{
"detail": [
{
"type": "dict_attributes_type",
"loc": ["body"],
"msg": "Input should be a valid dictionary or instance to extract fields from",
"input": '{"name": "Foo", "price": 50.5}',
"url": match_pydantic_error_url("dict_attributes_type"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body"],
"msg": "value is not a valid dict",
"type": "type_error.dict",
}
]
}
)
response = client.post( response = client.post(
"/items/", content=data, headers={"Content-Type": "application/not-really-json"} "/items/", content=data, headers={"Content-Type": "application/not-really-json"}
) )
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
assert response.json() == invalid_dict assert response.json() == IsDict(
{
"detail": [
{
"type": "dict_attributes_type",
"loc": ["body"],
"msg": "Input should be a valid dictionary or instance to extract fields from",
"input": '{"name": "Foo", "price": 50.5}',
"url": match_pydantic_error_url("dict_attributes_type"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body"],
"msg": "value is not a valid dict",
"type": "type_error.dict",
}
]
}
)
def test_other_exceptions(): def test_other_exceptions(client: TestClient):
with patch("json.loads", side_effect=Exception): with patch("json.loads", side_effect=Exception):
response = client.post("/items/", json={"test": "test2"}) response = client.post("/items/", json={"test": "test2"})
assert response.status_code == 400, response.text assert response.status_code == 400, response.text
def test_openapi_schema(): def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
assert response.json() == { assert response.json() == {
@ -243,8 +438,26 @@ def test_openapi_schema():
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"}, "price": {"title": "Price", "type": "number"},
"description": {"title": "Description", "type": "string"}, "description": IsDict(
"tax": {"title": "Tax", "type": "number"}, {
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Description", "type": "string"}
),
"tax": IsDict(
{
"title": "Tax",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Tax", "type": "number"}
),
}, },
}, },
"ValidationError": { "ValidationError": {

432
tests/test_tutorial/test_body/test_tutorial001_py310.py

@ -1,7 +1,9 @@
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py310 from ...utils import needs_py310
@ -14,86 +16,189 @@ def client():
return client return client
price_missing = { @needs_py310
"detail": [ def test_body_float(client: TestClient):
response = client.post("/items/", json={"name": "Foo", "price": 50.5})
assert response.status_code == 200
assert response.json() == {
"name": "Foo",
"price": 50.5,
"description": None,
"tax": None,
}
@needs_py310
def test_post_with_str_float(client: TestClient):
response = client.post("/items/", json={"name": "Foo", "price": "50.5"})
assert response.status_code == 200
assert response.json() == {
"name": "Foo",
"price": 50.5,
"description": None,
"tax": None,
}
@needs_py310
def test_post_with_str_float_description(client: TestClient):
response = client.post(
"/items/", json={"name": "Foo", "price": "50.5", "description": "Some Foo"}
)
assert response.status_code == 200
assert response.json() == {
"name": "Foo",
"price": 50.5,
"description": "Some Foo",
"tax": None,
}
@needs_py310
def test_post_with_str_float_description_tax(client: TestClient):
response = client.post(
"/items/",
json={"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3},
)
assert response.status_code == 200
assert response.json() == {
"name": "Foo",
"price": 50.5,
"description": "Some Foo",
"tax": 0.3,
}
@needs_py310
def test_post_with_only_name(client: TestClient):
response = client.post("/items/", json={"name": "Foo"})
assert response.status_code == 422
assert response.json() == IsDict(
{ {
"loc": ["body", "price"], "detail": [
"msg": "field required", {
"type": "value_error.missing", "type": "missing",
"loc": ["body", "price"],
"msg": "Field required",
"input": {"name": "Foo"},
"url": match_pydantic_error_url("missing"),
}
]
} }
] ) | IsDict(
} # TODO: remove when deprecating Pydantic v1
price_not_float = {
"detail": [
{ {
"loc": ["body", "price"], "detail": [
"msg": "value is not a valid float", {
"type": "type_error.float", "loc": ["body", "price"],
"msg": "field required",
"type": "value_error.missing",
}
]
} }
] )
}
name_price_missing = { @needs_py310
"detail": [ def test_post_with_only_name_price(client: TestClient):
response = client.post("/items/", json={"name": "Foo", "price": "twenty"})
assert response.status_code == 422
assert response.json() == IsDict(
{ {
"loc": ["body", "name"], "detail": [
"msg": "field required", {
"type": "value_error.missing", "type": "float_parsing",
}, "loc": ["body", "price"],
"msg": "Input should be a valid number, unable to parse string as an number",
"input": "twenty",
"url": match_pydantic_error_url("float_parsing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{ {
"loc": ["body", "price"], "detail": [
"msg": "field required", {
"type": "value_error.missing", "loc": ["body", "price"],
}, "msg": "value is not a valid float",
] "type": "type_error.float",
} }
]
}
)
body_missing = {
"detail": [ @needs_py310
{"loc": ["body"], "msg": "field required", "type": "value_error.missing"} def test_post_with_no_data(client: TestClient):
] response = client.post("/items/", json={})
} assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "name"],
"msg": "Field required",
"input": {},
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["body", "price"],
"msg": "Field required",
"input": {},
"url": match_pydantic_error_url("missing"),
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "name"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "price"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
@needs_py310 @needs_py310
@pytest.mark.parametrize( def test_post_with_none(client: TestClient):
"path,body,expected_status,expected_response", response = client.post("/items/", json=None)
[ assert response.status_code == 422
( assert response.json() == IsDict(
"/items/", {
{"name": "Foo", "price": 50.5}, "detail": [
200, {
{"name": "Foo", "price": 50.5, "description": None, "tax": None}, "type": "missing",
), "loc": ["body"],
( "msg": "Field required",
"/items/", "input": None,
{"name": "Foo", "price": "50.5"}, "url": match_pydantic_error_url("missing"),
200, }
{"name": "Foo", "price": 50.5, "description": None, "tax": None}, ]
), }
( ) | IsDict(
"/items/", # TODO: remove when deprecating Pydantic v1
{"name": "Foo", "price": "50.5", "description": "Some Foo"}, {
200, "detail": [
{"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": None}, {
), "loc": ["body"],
( "msg": "field required",
"/items/", "type": "value_error.missing",
{"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3}, }
200, ]
{"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": 0.3}, }
), )
("/items/", {"name": "Foo"}, 422, price_missing),
("/items/", {"name": "Foo", "price": "twenty"}, 422, price_not_float),
("/items/", {}, 422, name_price_missing),
("/items/", None, 422, body_missing),
],
)
def test_post_body(path, body, expected_status, expected_response, client: TestClient):
response = client.post(path, json=body)
assert response.status_code == expected_status
assert response.json() == expected_response
@needs_py310 @needs_py310
@ -104,37 +209,69 @@ def test_post_broken_body(client: TestClient):
content="{some broken json}", content="{some broken json}",
) )
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
assert response.json() == { assert response.json() == IsDict(
"detail": [ {
{ "detail": [
"loc": ["body", 1], {
"msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", "type": "json_invalid",
"type": "value_error.jsondecode", "loc": ["body", 1],
"ctx": { "msg": "JSON decode error",
"msg": "Expecting property name enclosed in double quotes", "input": {},
"doc": "{some broken json}", "ctx": {
"pos": 1, "error": "Expecting property name enclosed in double quotes"
"lineno": 1, },
"colno": 2, }
}, ]
} }
] ) | IsDict(
} # TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", 1],
"msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)",
"type": "value_error.jsondecode",
"ctx": {
"msg": "Expecting property name enclosed in double quotes",
"doc": "{some broken json}",
"pos": 1,
"lineno": 1,
"colno": 2,
},
}
]
}
)
@needs_py310 @needs_py310
def test_post_form_for_json(client: TestClient): def test_post_form_for_json(client: TestClient):
response = client.post("/items/", data={"name": "Foo", "price": 50.5}) response = client.post("/items/", data={"name": "Foo", "price": 50.5})
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
assert response.json() == { assert response.json() == IsDict(
"detail": [ {
{ "detail": [
"loc": ["body"], {
"msg": "value is not a valid dict", "type": "dict_attributes_type",
"type": "type_error.dict", "loc": ["body"],
} "msg": "Input should be a valid dictionary or instance to extract fields from",
] "input": "name=Foo&price=50.5",
} "url": match_pydantic_error_url("dict_attributes_type"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body"],
"msg": "value is not a valid dict",
"type": "type_error.dict",
}
]
}
)
@needs_py310 @needs_py310
@ -175,32 +312,91 @@ def test_no_content_type_is_json(client: TestClient):
@needs_py310 @needs_py310
def test_wrong_headers(client: TestClient): def test_wrong_headers(client: TestClient):
data = '{"name": "Foo", "price": 50.5}' data = '{"name": "Foo", "price": 50.5}'
invalid_dict = {
"detail": [
{
"loc": ["body"],
"msg": "value is not a valid dict",
"type": "type_error.dict",
}
]
}
response = client.post( response = client.post(
"/items/", content=data, headers={"Content-Type": "text/plain"} "/items/", content=data, headers={"Content-Type": "text/plain"}
) )
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
assert response.json() == invalid_dict assert response.json() == IsDict(
{
"detail": [
{
"type": "dict_attributes_type",
"loc": ["body"],
"msg": "Input should be a valid dictionary or instance to extract fields from",
"input": '{"name": "Foo", "price": 50.5}',
"url": match_pydantic_error_url("dict_attributes_type"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body"],
"msg": "value is not a valid dict",
"type": "type_error.dict",
}
]
}
)
response = client.post( response = client.post(
"/items/", content=data, headers={"Content-Type": "application/geo+json-seq"} "/items/", content=data, headers={"Content-Type": "application/geo+json-seq"}
) )
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
assert response.json() == invalid_dict assert response.json() == IsDict(
{
"detail": [
{
"type": "dict_attributes_type",
"loc": ["body"],
"msg": "Input should be a valid dictionary or instance to extract fields from",
"input": '{"name": "Foo", "price": 50.5}',
"url": match_pydantic_error_url("dict_attributes_type"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body"],
"msg": "value is not a valid dict",
"type": "type_error.dict",
}
]
}
)
response = client.post( response = client.post(
"/items/", content=data, headers={"Content-Type": "application/not-really-json"} "/items/", content=data, headers={"Content-Type": "application/not-really-json"}
) )
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
assert response.json() == invalid_dict assert response.json() == IsDict(
{
"detail": [
{
"type": "dict_attributes_type",
"loc": ["body"],
"msg": "Input should be a valid dictionary or instance to extract fields from",
"input": '{"name": "Foo", "price": 50.5}',
"url": match_pydantic_error_url("dict_attributes_type"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body"],
"msg": "value is not a valid dict",
"type": "type_error.dict",
}
]
}
)
@needs_py310 @needs_py310
@ -258,8 +454,26 @@ def test_openapi_schema(client: TestClient):
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"}, "price": {"title": "Price", "type": "number"},
"description": {"title": "Description", "type": "string"}, "description": IsDict(
"tax": {"title": "Tax", "type": "number"}, {
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Description", "type": "string"}
),
"tax": IsDict(
{
"title": "Tax",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Tax", "type": "number"}
),
}, },
}, },
"ValidationError": { "ValidationError": {

153
tests/test_tutorial/test_body_fields/test_tutorial001.py

@ -1,66 +1,82 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from docs_src.body_fields.tutorial001 import app
client = TestClient(app) @pytest.fixture(name="client")
def get_client():
from docs_src.body_fields.tutorial001 import app
client = TestClient(app)
return client
price_not_greater = {
"detail": [ def test_items_5(client: TestClient):
{ response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}})
"ctx": {"limit_value": 0}, assert response.status_code == 200
"loc": ["body", "item", "price"], assert response.json() == {
"msg": "ensure this value is greater than 0", "item_id": 5,
"type": "value_error.number.not_gt", "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
} }
]
}
def test_items_6(client: TestClient):
response = client.put(
"/items/6",
json={
"item": {
"name": "Bar",
"price": 0.2,
"description": "Some bar",
"tax": "5.4",
}
},
)
assert response.status_code == 200
assert response.json() == {
"item_id": 6,
"item": {
"name": "Bar",
"price": 0.2,
"description": "Some bar",
"tax": 5.4,
},
}
@pytest.mark.parametrize( def test_invalid_price(client: TestClient):
"path,body,expected_status,expected_response", response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}})
[ assert response.status_code == 422
( assert response.json() == IsDict(
"/items/5", {
{"item": {"name": "Foo", "price": 3.0}}, "detail": [
200, {
{ "type": "greater_than",
"item_id": 5, "loc": ["body", "item", "price"],
"item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, "msg": "Input should be greater than 0",
}, "input": -3.0,
), "ctx": {"gt": 0.0},
( "url": match_pydantic_error_url("greater_than"),
"/items/6",
{
"item": {
"name": "Bar",
"price": 0.2,
"description": "Some bar",
"tax": "5.4",
} }
}, ]
200, }
{ ) | IsDict(
"item_id": 6, # TODO: remove when deprecating Pydantic v1
"item": { {
"name": "Bar", "detail": [
"price": 0.2, {
"description": "Some bar", "ctx": {"limit_value": 0},
"tax": 5.4, "loc": ["body", "item", "price"],
}, "msg": "ensure this value is greater than 0",
}, "type": "value_error.number.not_gt",
), }
("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), ]
], }
) )
def test(path, body, expected_status, expected_response):
response = client.put(path, json=body)
assert response.status_code == expected_status
assert response.json() == expected_response
def test_openapi_schema(): def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
assert response.json() == { assert response.json() == {
@ -116,18 +132,39 @@ def test_openapi_schema():
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"description": { "description": IsDict(
"title": "The description of the item", {
"maxLength": 300, "title": "The description of the item",
"type": "string", "anyOf": [
}, {"maxLength": 300, "type": "string"},
{"type": "null"},
],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "The description of the item",
"maxLength": 300,
"type": "string",
}
),
"price": { "price": {
"title": "Price", "title": "Price",
"exclusiveMinimum": 0.0, "exclusiveMinimum": 0.0,
"type": "number", "type": "number",
"description": "The price must be greater than zero", "description": "The price must be greater than zero",
}, },
"tax": {"title": "Tax", "type": "number"}, "tax": IsDict(
{
"title": "Tax",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Tax", "type": "number"}
),
}, },
}, },
"Body_update_item_items__item_id__put": { "Body_update_item_items__item_id__put": {

153
tests/test_tutorial/test_body_fields/test_tutorial001_an.py

@ -1,66 +1,82 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from docs_src.body_fields.tutorial001_an import app
client = TestClient(app) @pytest.fixture(name="client")
def get_client():
from docs_src.body_fields.tutorial001_an import app
client = TestClient(app)
return client
price_not_greater = {
"detail": [ def test_items_5(client: TestClient):
{ response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}})
"ctx": {"limit_value": 0}, assert response.status_code == 200
"loc": ["body", "item", "price"], assert response.json() == {
"msg": "ensure this value is greater than 0", "item_id": 5,
"type": "value_error.number.not_gt", "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
} }
]
}
def test_items_6(client: TestClient):
response = client.put(
"/items/6",
json={
"item": {
"name": "Bar",
"price": 0.2,
"description": "Some bar",
"tax": "5.4",
}
},
)
assert response.status_code == 200
assert response.json() == {
"item_id": 6,
"item": {
"name": "Bar",
"price": 0.2,
"description": "Some bar",
"tax": 5.4,
},
}
@pytest.mark.parametrize( def test_invalid_price(client: TestClient):
"path,body,expected_status,expected_response", response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}})
[ assert response.status_code == 422
( assert response.json() == IsDict(
"/items/5", {
{"item": {"name": "Foo", "price": 3.0}}, "detail": [
200, {
{ "type": "greater_than",
"item_id": 5, "loc": ["body", "item", "price"],
"item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, "msg": "Input should be greater than 0",
}, "input": -3.0,
), "ctx": {"gt": 0.0},
( "url": match_pydantic_error_url("greater_than"),
"/items/6",
{
"item": {
"name": "Bar",
"price": 0.2,
"description": "Some bar",
"tax": "5.4",
} }
}, ]
200, }
{ ) | IsDict(
"item_id": 6, # TODO: remove when deprecating Pydantic v1
"item": { {
"name": "Bar", "detail": [
"price": 0.2, {
"description": "Some bar", "ctx": {"limit_value": 0},
"tax": 5.4, "loc": ["body", "item", "price"],
}, "msg": "ensure this value is greater than 0",
}, "type": "value_error.number.not_gt",
), }
("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), ]
], }
) )
def test(path, body, expected_status, expected_response):
response = client.put(path, json=body)
assert response.status_code == expected_status
assert response.json() == expected_response
def test_openapi_schema(): def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
assert response.json() == { assert response.json() == {
@ -116,18 +132,39 @@ def test_openapi_schema():
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"description": { "description": IsDict(
"title": "The description of the item", {
"maxLength": 300, "title": "The description of the item",
"type": "string", "anyOf": [
}, {"maxLength": 300, "type": "string"},
{"type": "null"},
],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "The description of the item",
"maxLength": 300,
"type": "string",
}
),
"price": { "price": {
"title": "Price", "title": "Price",
"exclusiveMinimum": 0.0, "exclusiveMinimum": 0.0,
"type": "number", "type": "number",
"description": "The price must be greater than zero", "description": "The price must be greater than zero",
}, },
"tax": {"title": "Tax", "type": "number"}, "tax": IsDict(
{
"title": "Tax",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Tax", "type": "number"}
),
}, },
}, },
"Body_update_item_items__item_id__put": { "Body_update_item_items__item_id__put": {

145
tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py

@ -1,5 +1,7 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py310 from ...utils import needs_py310
@ -12,59 +14,71 @@ def get_client():
return client return client
price_not_greater = { @needs_py310
"detail": [ def test_items_5(client: TestClient):
{ response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}})
"ctx": {"limit_value": 0}, assert response.status_code == 200
"loc": ["body", "item", "price"], assert response.json() == {
"msg": "ensure this value is greater than 0", "item_id": 5,
"type": "value_error.number.not_gt", "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
} }
]
}
@needs_py310 @needs_py310
@pytest.mark.parametrize( def test_items_6(client: TestClient):
"path,body,expected_status,expected_response", response = client.put(
[ "/items/6",
( json={
"/items/5", "item": {
{"item": {"name": "Foo", "price": 3.0}}, "name": "Bar",
200, "price": 0.2,
{ "description": "Some bar",
"item_id": 5, "tax": "5.4",
"item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, }
}, },
), )
( assert response.status_code == 200
"/items/6", assert response.json() == {
{ "item_id": 6,
"item": { "item": {
"name": "Bar", "name": "Bar",
"price": 0.2, "price": 0.2,
"description": "Some bar", "description": "Some bar",
"tax": "5.4", "tax": 5.4,
},
}
@needs_py310
def test_invalid_price(client: TestClient):
response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "greater_than",
"loc": ["body", "item", "price"],
"msg": "Input should be greater than 0",
"input": -3.0,
"ctx": {"gt": 0.0},
"url": match_pydantic_error_url("greater_than"),
} }
}, ]
200, }
{ ) | IsDict(
"item_id": 6, # TODO: remove when deprecating Pydantic v1
"item": { {
"name": "Bar", "detail": [
"price": 0.2, {
"description": "Some bar", "ctx": {"limit_value": 0},
"tax": 5.4, "loc": ["body", "item", "price"],
}, "msg": "ensure this value is greater than 0",
}, "type": "value_error.number.not_gt",
), }
("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), ]
], }
) )
def test(path, body, expected_status, expected_response, client: TestClient):
response = client.put(path, json=body)
assert response.status_code == expected_status
assert response.json() == expected_response
@needs_py310 @needs_py310
@ -124,18 +138,39 @@ def test_openapi_schema(client: TestClient):
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"description": { "description": IsDict(
"title": "The description of the item", {
"maxLength": 300, "title": "The description of the item",
"type": "string", "anyOf": [
}, {"maxLength": 300, "type": "string"},
{"type": "null"},
],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "The description of the item",
"maxLength": 300,
"type": "string",
}
),
"price": { "price": {
"title": "Price", "title": "Price",
"exclusiveMinimum": 0.0, "exclusiveMinimum": 0.0,
"type": "number", "type": "number",
"description": "The price must be greater than zero", "description": "The price must be greater than zero",
}, },
"tax": {"title": "Tax", "type": "number"}, "tax": IsDict(
{
"title": "Tax",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Tax", "type": "number"}
),
}, },
}, },
"Body_update_item_items__item_id__put": { "Body_update_item_items__item_id__put": {

145
tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py

@ -1,5 +1,7 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py39 from ...utils import needs_py39
@ -12,59 +14,71 @@ def get_client():
return client return client
price_not_greater = { @needs_py39
"detail": [ def test_items_5(client: TestClient):
{ response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}})
"ctx": {"limit_value": 0}, assert response.status_code == 200
"loc": ["body", "item", "price"], assert response.json() == {
"msg": "ensure this value is greater than 0", "item_id": 5,
"type": "value_error.number.not_gt", "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
} }
]
}
@needs_py39 @needs_py39
@pytest.mark.parametrize( def test_items_6(client: TestClient):
"path,body,expected_status,expected_response", response = client.put(
[ "/items/6",
( json={
"/items/5", "item": {
{"item": {"name": "Foo", "price": 3.0}}, "name": "Bar",
200, "price": 0.2,
{ "description": "Some bar",
"item_id": 5, "tax": "5.4",
"item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, }
}, },
), )
( assert response.status_code == 200
"/items/6", assert response.json() == {
{ "item_id": 6,
"item": { "item": {
"name": "Bar", "name": "Bar",
"price": 0.2, "price": 0.2,
"description": "Some bar", "description": "Some bar",
"tax": "5.4", "tax": 5.4,
},
}
@needs_py39
def test_invalid_price(client: TestClient):
response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "greater_than",
"loc": ["body", "item", "price"],
"msg": "Input should be greater than 0",
"input": -3.0,
"ctx": {"gt": 0.0},
"url": match_pydantic_error_url("greater_than"),
} }
}, ]
200, }
{ ) | IsDict(
"item_id": 6, # TODO: remove when deprecating Pydantic v1
"item": { {
"name": "Bar", "detail": [
"price": 0.2, {
"description": "Some bar", "ctx": {"limit_value": 0},
"tax": 5.4, "loc": ["body", "item", "price"],
}, "msg": "ensure this value is greater than 0",
}, "type": "value_error.number.not_gt",
), }
("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), ]
], }
) )
def test(path, body, expected_status, expected_response, client: TestClient):
response = client.put(path, json=body)
assert response.status_code == expected_status
assert response.json() == expected_response
@needs_py39 @needs_py39
@ -124,18 +138,39 @@ def test_openapi_schema(client: TestClient):
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"description": { "description": IsDict(
"title": "The description of the item", {
"maxLength": 300, "title": "The description of the item",
"type": "string", "anyOf": [
}, {"maxLength": 300, "type": "string"},
{"type": "null"},
],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "The description of the item",
"maxLength": 300,
"type": "string",
}
),
"price": { "price": {
"title": "Price", "title": "Price",
"exclusiveMinimum": 0.0, "exclusiveMinimum": 0.0,
"type": "number", "type": "number",
"description": "The price must be greater than zero", "description": "The price must be greater than zero",
}, },
"tax": {"title": "Tax", "type": "number"}, "tax": IsDict(
{
"title": "Tax",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Tax", "type": "number"}
),
}, },
}, },
"Body_update_item_items__item_id__put": { "Body_update_item_items__item_id__put": {

145
tests/test_tutorial/test_body_fields/test_tutorial001_py310.py

@ -1,5 +1,7 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py310 from ...utils import needs_py310
@ -12,59 +14,71 @@ def get_client():
return client return client
price_not_greater = { @needs_py310
"detail": [ def test_items_5(client: TestClient):
{ response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}})
"ctx": {"limit_value": 0}, assert response.status_code == 200
"loc": ["body", "item", "price"], assert response.json() == {
"msg": "ensure this value is greater than 0", "item_id": 5,
"type": "value_error.number.not_gt", "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
} }
]
}
@needs_py310 @needs_py310
@pytest.mark.parametrize( def test_items_6(client: TestClient):
"path,body,expected_status,expected_response", response = client.put(
[ "/items/6",
( json={
"/items/5", "item": {
{"item": {"name": "Foo", "price": 3.0}}, "name": "Bar",
200, "price": 0.2,
{ "description": "Some bar",
"item_id": 5, "tax": "5.4",
"item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, }
}, },
), )
( assert response.status_code == 200
"/items/6", assert response.json() == {
{ "item_id": 6,
"item": { "item": {
"name": "Bar", "name": "Bar",
"price": 0.2, "price": 0.2,
"description": "Some bar", "description": "Some bar",
"tax": "5.4", "tax": 5.4,
},
}
@needs_py310
def test_invalid_price(client: TestClient):
response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "greater_than",
"loc": ["body", "item", "price"],
"msg": "Input should be greater than 0",
"input": -3.0,
"ctx": {"gt": 0.0},
"url": match_pydantic_error_url("greater_than"),
} }
}, ]
200, }
{ ) | IsDict(
"item_id": 6, # TODO: remove when deprecating Pydantic v1
"item": { {
"name": "Bar", "detail": [
"price": 0.2, {
"description": "Some bar", "ctx": {"limit_value": 0},
"tax": 5.4, "loc": ["body", "item", "price"],
}, "msg": "ensure this value is greater than 0",
}, "type": "value_error.number.not_gt",
), }
("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), ]
], }
) )
def test(path, body, expected_status, expected_response, client: TestClient):
response = client.put(path, json=body)
assert response.status_code == expected_status
assert response.json() == expected_response
@needs_py310 @needs_py310
@ -124,18 +138,39 @@ def test_openapi_schema(client: TestClient):
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"description": { "description": IsDict(
"title": "The description of the item", {
"maxLength": 300, "title": "The description of the item",
"type": "string", "anyOf": [
}, {"maxLength": 300, "type": "string"},
{"type": "null"},
],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "The description of the item",
"maxLength": 300,
"type": "string",
}
),
"price": { "price": {
"title": "Price", "title": "Price",
"exclusiveMinimum": 0.0, "exclusiveMinimum": 0.0,
"type": "number", "type": "number",
"description": "The price must be greater than zero", "description": "The price must be greater than zero",
}, },
"tax": {"title": "Tax", "type": "number"}, "tax": IsDict(
{
"title": "Tax",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Tax", "type": "number"}
),
}, },
}, },
"Body_update_item_items__item_id__put": { "Body_update_item_items__item_id__put": {

147
tests/test_tutorial/test_body_multiple_params/test_tutorial001.py

@ -1,52 +1,74 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from docs_src.body_multiple_params.tutorial001 import app
client = TestClient(app) @pytest.fixture(name="client")
def get_client():
from docs_src.body_multiple_params.tutorial001 import app
client = TestClient(app)
return client
item_id_not_int = {
"detail": [
{
"loc": ["path", "item_id"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
def test_post_body_q_bar_content(client: TestClient):
response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5})
assert response.status_code == 200
assert response.json() == {
"item_id": 5,
"item": {
"name": "Foo",
"price": 50.5,
"description": None,
"tax": None,
},
"q": "bar",
}
@pytest.mark.parametrize(
"path,body,expected_status,expected_response",
[
(
"/items/5?q=bar",
{"name": "Foo", "price": 50.5},
200,
{
"item_id": 5,
"item": {
"name": "Foo",
"price": 50.5,
"description": None,
"tax": None,
},
"q": "bar",
},
),
("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}),
("/items/5", None, 200, {"item_id": 5}),
("/items/foo", None, 422, item_id_not_int),
],
)
def test_post_body(path, body, expected_status, expected_response):
response = client.put(path, json=body)
assert response.status_code == expected_status
assert response.json() == expected_response
def test_post_no_body_q_bar(client: TestClient):
response = client.put("/items/5?q=bar", json=None)
assert response.status_code == 200
assert response.json() == {"item_id": 5, "q": "bar"}
def test_openapi_schema():
def test_post_no_body(client: TestClient):
response = client.put("/items/5", json=None)
assert response.status_code == 200
assert response.json() == {"item_id": 5}
def test_post_id_foo(client: TestClient):
response = client.put("/items/foo", json=None)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"loc": ["path", "item_id"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "foo",
"url": match_pydantic_error_url("int_parsing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["path", "item_id"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
assert response.json() == { assert response.json() == {
@ -87,7 +109,16 @@ def test_openapi_schema():
}, },
{ {
"required": False, "required": False,
"schema": {"title": "Q", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Q",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Q", "type": "string"}
),
"name": "q", "name": "q",
"in": "query", "in": "query",
}, },
@ -95,7 +126,19 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/Item"} "schema": IsDict(
{
"anyOf": [
{"$ref": "#/components/schemas/Item"},
{"type": "null"},
],
"title": "Item",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"$ref": "#/components/schemas/Item"}
)
} }
} }
}, },
@ -110,9 +153,27 @@ def test_openapi_schema():
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"description": IsDict(
{
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Description", "type": "string"}
),
"price": {"title": "Price", "type": "number"}, "price": {"title": "Price", "type": "number"},
"description": {"title": "Description", "type": "string"}, "tax": IsDict(
"tax": {"title": "Tax", "type": "number"}, {
"title": "Tax",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Tax", "type": "number"}
),
}, },
}, },
"ValidationError": { "ValidationError": {

147
tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py

@ -1,52 +1,74 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from docs_src.body_multiple_params.tutorial001_an import app
client = TestClient(app) @pytest.fixture(name="client")
def get_client():
from docs_src.body_multiple_params.tutorial001_an import app
client = TestClient(app)
return client
item_id_not_int = {
"detail": [
{
"loc": ["path", "item_id"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
def test_post_body_q_bar_content(client: TestClient):
response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5})
assert response.status_code == 200
assert response.json() == {
"item_id": 5,
"item": {
"name": "Foo",
"price": 50.5,
"description": None,
"tax": None,
},
"q": "bar",
}
@pytest.mark.parametrize(
"path,body,expected_status,expected_response",
[
(
"/items/5?q=bar",
{"name": "Foo", "price": 50.5},
200,
{
"item_id": 5,
"item": {
"name": "Foo",
"price": 50.5,
"description": None,
"tax": None,
},
"q": "bar",
},
),
("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}),
("/items/5", None, 200, {"item_id": 5}),
("/items/foo", None, 422, item_id_not_int),
],
)
def test_post_body(path, body, expected_status, expected_response):
response = client.put(path, json=body)
assert response.status_code == expected_status
assert response.json() == expected_response
def test_post_no_body_q_bar(client: TestClient):
response = client.put("/items/5?q=bar", json=None)
assert response.status_code == 200
assert response.json() == {"item_id": 5, "q": "bar"}
def test_openapi_schema():
def test_post_no_body(client: TestClient):
response = client.put("/items/5", json=None)
assert response.status_code == 200
assert response.json() == {"item_id": 5}
def test_post_id_foo(client: TestClient):
response = client.put("/items/foo", json=None)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"loc": ["path", "item_id"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "foo",
"url": match_pydantic_error_url("int_parsing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["path", "item_id"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
assert response.json() == { assert response.json() == {
@ -87,7 +109,16 @@ def test_openapi_schema():
}, },
{ {
"required": False, "required": False,
"schema": {"title": "Q", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Q",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Q", "type": "string"}
),
"name": "q", "name": "q",
"in": "query", "in": "query",
}, },
@ -95,7 +126,19 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/Item"} "schema": IsDict(
{
"anyOf": [
{"$ref": "#/components/schemas/Item"},
{"type": "null"},
],
"title": "Item",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"$ref": "#/components/schemas/Item"}
)
} }
} }
}, },
@ -110,9 +153,27 @@ def test_openapi_schema():
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"description": IsDict(
{
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Description", "type": "string"}
),
"price": {"title": "Price", "type": "number"}, "price": {"title": "Price", "type": "number"},
"description": {"title": "Description", "type": "string"}, "tax": IsDict(
"tax": {"title": "Tax", "type": "number"}, {
"title": "Tax",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Tax", "type": "number"}
),
}, },
}, },
"ValidationError": { "ValidationError": {

140
tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py

@ -1,5 +1,7 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py310 from ...utils import needs_py310
@ -12,45 +14,64 @@ def get_client():
return client return client
item_id_not_int = { @needs_py310
"detail": [ def test_post_body_q_bar_content(client: TestClient):
{ response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5})
"loc": ["path", "item_id"], assert response.status_code == 200
"msg": "value is not a valid integer", assert response.json() == {
"type": "type_error.integer", "item_id": 5,
} "item": {
] "name": "Foo",
} "price": 50.5,
"description": None,
"tax": None,
},
"q": "bar",
}
@needs_py310 @needs_py310
@pytest.mark.parametrize( def test_post_no_body_q_bar(client: TestClient):
"path,body,expected_status,expected_response", response = client.put("/items/5?q=bar", json=None)
[ assert response.status_code == 200
( assert response.json() == {"item_id": 5, "q": "bar"}
"/items/5?q=bar",
{"name": "Foo", "price": 50.5},
200, @needs_py310
{ def test_post_no_body(client: TestClient):
"item_id": 5, response = client.put("/items/5", json=None)
"item": { assert response.status_code == 200
"name": "Foo", assert response.json() == {"item_id": 5}
"price": 50.5,
"description": None,
"tax": None, @needs_py310
}, def test_post_id_foo(client: TestClient):
"q": "bar", response = client.put("/items/foo", json=None)
}, assert response.status_code == 422
), assert response.json() == IsDict(
("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), {
("/items/5", None, 200, {"item_id": 5}), "detail": [
("/items/foo", None, 422, item_id_not_int), {
], "type": "int_parsing",
) "loc": ["path", "item_id"],
def test_post_body(path, body, expected_status, expected_response, client: TestClient): "msg": "Input should be a valid integer, unable to parse string as an integer",
response = client.put(path, json=body) "input": "foo",
assert response.status_code == expected_status "url": match_pydantic_error_url("int_parsing"),
assert response.json() == expected_response }
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["path", "item_id"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
@needs_py310 @needs_py310
@ -95,7 +116,16 @@ def test_openapi_schema(client: TestClient):
}, },
{ {
"required": False, "required": False,
"schema": {"title": "Q", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Q",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Q", "type": "string"}
),
"name": "q", "name": "q",
"in": "query", "in": "query",
}, },
@ -103,7 +133,19 @@ def test_openapi_schema(client: TestClient):
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/Item"} "schema": IsDict(
{
"anyOf": [
{"$ref": "#/components/schemas/Item"},
{"type": "null"},
],
"title": "Item",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"$ref": "#/components/schemas/Item"}
)
} }
} }
}, },
@ -118,9 +160,27 @@ def test_openapi_schema(client: TestClient):
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"description": IsDict(
{
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Description", "type": "string"}
),
"price": {"title": "Price", "type": "number"}, "price": {"title": "Price", "type": "number"},
"description": {"title": "Description", "type": "string"}, "tax": IsDict(
"tax": {"title": "Tax", "type": "number"}, {
"title": "Tax",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Tax", "type": "number"}
),
}, },
}, },
"ValidationError": { "ValidationError": {

140
tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py

@ -1,5 +1,7 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py39 from ...utils import needs_py39
@ -12,45 +14,64 @@ def get_client():
return client return client
item_id_not_int = { @needs_py39
"detail": [ def test_post_body_q_bar_content(client: TestClient):
{ response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5})
"loc": ["path", "item_id"], assert response.status_code == 200
"msg": "value is not a valid integer", assert response.json() == {
"type": "type_error.integer", "item_id": 5,
} "item": {
] "name": "Foo",
} "price": 50.5,
"description": None,
"tax": None,
},
"q": "bar",
}
@needs_py39 @needs_py39
@pytest.mark.parametrize( def test_post_no_body_q_bar(client: TestClient):
"path,body,expected_status,expected_response", response = client.put("/items/5?q=bar", json=None)
[ assert response.status_code == 200
( assert response.json() == {"item_id": 5, "q": "bar"}
"/items/5?q=bar",
{"name": "Foo", "price": 50.5},
200, @needs_py39
{ def test_post_no_body(client: TestClient):
"item_id": 5, response = client.put("/items/5", json=None)
"item": { assert response.status_code == 200
"name": "Foo", assert response.json() == {"item_id": 5}
"price": 50.5,
"description": None,
"tax": None, @needs_py39
}, def test_post_id_foo(client: TestClient):
"q": "bar", response = client.put("/items/foo", json=None)
}, assert response.status_code == 422
), assert response.json() == IsDict(
("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), {
("/items/5", None, 200, {"item_id": 5}), "detail": [
("/items/foo", None, 422, item_id_not_int), {
], "type": "int_parsing",
) "loc": ["path", "item_id"],
def test_post_body(path, body, expected_status, expected_response, client: TestClient): "msg": "Input should be a valid integer, unable to parse string as an integer",
response = client.put(path, json=body) "input": "foo",
assert response.status_code == expected_status "url": match_pydantic_error_url("int_parsing"),
assert response.json() == expected_response }
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["path", "item_id"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
@needs_py39 @needs_py39
@ -95,7 +116,16 @@ def test_openapi_schema(client: TestClient):
}, },
{ {
"required": False, "required": False,
"schema": {"title": "Q", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Q",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Q", "type": "string"}
),
"name": "q", "name": "q",
"in": "query", "in": "query",
}, },
@ -103,7 +133,19 @@ def test_openapi_schema(client: TestClient):
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/Item"} "schema": IsDict(
{
"anyOf": [
{"$ref": "#/components/schemas/Item"},
{"type": "null"},
],
"title": "Item",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"$ref": "#/components/schemas/Item"}
)
} }
} }
}, },
@ -118,9 +160,27 @@ def test_openapi_schema(client: TestClient):
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"description": IsDict(
{
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Description", "type": "string"}
),
"price": {"title": "Price", "type": "number"}, "price": {"title": "Price", "type": "number"},
"description": {"title": "Description", "type": "string"}, "tax": IsDict(
"tax": {"title": "Tax", "type": "number"}, {
"title": "Tax",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Tax", "type": "number"}
),
}, },
}, },
"ValidationError": { "ValidationError": {

140
tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py

@ -1,5 +1,7 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py310 from ...utils import needs_py310
@ -12,45 +14,64 @@ def get_client():
return client return client
item_id_not_int = { @needs_py310
"detail": [ def test_post_body_q_bar_content(client: TestClient):
{ response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5})
"loc": ["path", "item_id"], assert response.status_code == 200
"msg": "value is not a valid integer", assert response.json() == {
"type": "type_error.integer", "item_id": 5,
} "item": {
] "name": "Foo",
} "price": 50.5,
"description": None,
"tax": None,
},
"q": "bar",
}
@needs_py310 @needs_py310
@pytest.mark.parametrize( def test_post_no_body_q_bar(client: TestClient):
"path,body,expected_status,expected_response", response = client.put("/items/5?q=bar", json=None)
[ assert response.status_code == 200
( assert response.json() == {"item_id": 5, "q": "bar"}
"/items/5?q=bar",
{"name": "Foo", "price": 50.5},
200, @needs_py310
{ def test_post_no_body(client: TestClient):
"item_id": 5, response = client.put("/items/5", json=None)
"item": { assert response.status_code == 200
"name": "Foo", assert response.json() == {"item_id": 5}
"price": 50.5,
"description": None,
"tax": None, @needs_py310
}, def test_post_id_foo(client: TestClient):
"q": "bar", response = client.put("/items/foo", json=None)
}, assert response.status_code == 422
), assert response.json() == IsDict(
("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), {
("/items/5", None, 200, {"item_id": 5}), "detail": [
("/items/foo", None, 422, item_id_not_int), {
], "type": "int_parsing",
) "loc": ["path", "item_id"],
def test_post_body(path, body, expected_status, expected_response, client: TestClient): "msg": "Input should be a valid integer, unable to parse string as an integer",
response = client.put(path, json=body) "input": "foo",
assert response.status_code == expected_status "url": match_pydantic_error_url("int_parsing"),
assert response.json() == expected_response }
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["path", "item_id"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
@needs_py310 @needs_py310
@ -95,7 +116,16 @@ def test_openapi_schema(client: TestClient):
}, },
{ {
"required": False, "required": False,
"schema": {"title": "Q", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Q",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Q", "type": "string"}
),
"name": "q", "name": "q",
"in": "query", "in": "query",
}, },
@ -103,7 +133,19 @@ def test_openapi_schema(client: TestClient):
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/Item"} "schema": IsDict(
{
"anyOf": [
{"$ref": "#/components/schemas/Item"},
{"type": "null"},
],
"title": "Item",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"$ref": "#/components/schemas/Item"}
)
} }
} }
}, },
@ -118,9 +160,27 @@ def test_openapi_schema(client: TestClient):
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"description": IsDict(
{
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Description", "type": "string"}
),
"price": {"title": "Price", "type": "number"}, "price": {"title": "Price", "type": "number"},
"description": {"title": "Description", "type": "string"}, "tax": IsDict(
"tax": {"title": "Tax", "type": "number"}, {
"title": "Tax",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Tax", "type": "number"}
),
}, },
}, },
"ValidationError": { "ValidationError": {

250
tests/test_tutorial/test_body_multiple_params/test_tutorial003.py

@ -1,92 +1,147 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from docs_src.body_multiple_params.tutorial003 import app
client = TestClient(app) @pytest.fixture(name="client")
def get_client():
from docs_src.body_multiple_params.tutorial003 import app
client = TestClient(app)
return client
# Test required and embedded body parameters with no bodies sent
@pytest.mark.parametrize( def test_post_body_valid(client: TestClient):
"path,body,expected_status,expected_response", response = client.put(
[ "/items/5",
( json={
"/items/5", "importance": 2,
{ "item": {"name": "Foo", "price": 50.5},
"importance": 2, "user": {"username": "Dave"},
"item": {"name": "Foo", "price": 50.5}, },
"user": {"username": "Dave"}, )
}, assert response.status_code == 200
200, assert response.json() == {
{ "item_id": 5,
"item_id": 5, "importance": 2,
"importance": 2, "item": {
"item": { "name": "Foo",
"name": "Foo", "price": 50.5,
"price": 50.5, "description": None,
"description": None, "tax": None,
"tax": None, },
}, "user": {"username": "Dave", "full_name": None},
"user": {"username": "Dave", "full_name": None}, }
},
),
( def test_post_body_no_data(client: TestClient):
"/items/5", response = client.put("/items/5", json=None)
None, assert response.status_code == 422
422, assert response.json() == IsDict(
{ {
"detail": [ "detail": [
{ {
"loc": ["body", "item"], "type": "missing",
"msg": "field required", "loc": ["body", "item"],
"type": "value_error.missing", "msg": "Field required",
}, "input": None,
{ "url": match_pydantic_error_url("missing"),
"loc": ["body", "user"], },
"msg": "field required", {
"type": "value_error.missing", "type": "missing",
}, "loc": ["body", "user"],
{ "msg": "Field required",
"loc": ["body", "importance"], "input": None,
"msg": "field required", "url": match_pydantic_error_url("missing"),
"type": "value_error.missing", },
}, {
] "type": "missing",
}, "loc": ["body", "importance"],
), "msg": "Field required",
( "input": None,
"/items/5", "url": match_pydantic_error_url("missing"),
[], },
422, ]
{ }
"detail": [ ) | IsDict(
{ # TODO: remove when deprecating Pydantic v1
"loc": ["body", "item"], {
"msg": "field required", "detail": [
"type": "value_error.missing", {
}, "loc": ["body", "item"],
{ "msg": "field required",
"loc": ["body", "user"], "type": "value_error.missing",
"msg": "field required", },
"type": "value_error.missing", {
}, "loc": ["body", "user"],
{ "msg": "field required",
"loc": ["body", "importance"], "type": "value_error.missing",
"msg": "field required", },
"type": "value_error.missing", {
}, "loc": ["body", "importance"],
] "msg": "field required",
}, "type": "value_error.missing",
), },
], ]
) }
def test_post_body(path, body, expected_status, expected_response): )
response = client.put(path, json=body)
assert response.status_code == expected_status
assert response.json() == expected_response
def test_openapi_schema(): def test_post_body_empty_list(client: TestClient):
response = client.put("/items/5", json=[])
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "item"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["body", "user"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["body", "importance"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "item"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "user"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "importance"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
assert response.json() == { assert response.json() == {
@ -142,9 +197,27 @@ def test_openapi_schema():
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"description": IsDict(
{
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Description", "type": "string"}
),
"price": {"title": "Price", "type": "number"}, "price": {"title": "Price", "type": "number"},
"description": {"title": "Description", "type": "string"}, "tax": IsDict(
"tax": {"title": "Tax", "type": "number"}, {
"title": "Tax",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Tax", "type": "number"}
),
}, },
}, },
"User": { "User": {
@ -153,7 +226,16 @@ def test_openapi_schema():
"type": "object", "type": "object",
"properties": { "properties": {
"username": {"title": "Username", "type": "string"}, "username": {"title": "Username", "type": "string"},
"full_name": {"title": "Full Name", "type": "string"}, "full_name": IsDict(
{
"title": "Full Name",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Full Name", "type": "string"}
),
}, },
}, },
"Body_update_item_items__item_id__put": { "Body_update_item_items__item_id__put": {

250
tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py

@ -1,92 +1,147 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from docs_src.body_multiple_params.tutorial003_an import app
client = TestClient(app) @pytest.fixture(name="client")
def get_client():
from docs_src.body_multiple_params.tutorial003_an import app
client = TestClient(app)
return client
# Test required and embedded body parameters with no bodies sent
@pytest.mark.parametrize( def test_post_body_valid(client: TestClient):
"path,body,expected_status,expected_response", response = client.put(
[ "/items/5",
( json={
"/items/5", "importance": 2,
{ "item": {"name": "Foo", "price": 50.5},
"importance": 2, "user": {"username": "Dave"},
"item": {"name": "Foo", "price": 50.5}, },
"user": {"username": "Dave"}, )
}, assert response.status_code == 200
200, assert response.json() == {
{ "item_id": 5,
"item_id": 5, "importance": 2,
"importance": 2, "item": {
"item": { "name": "Foo",
"name": "Foo", "price": 50.5,
"price": 50.5, "description": None,
"description": None, "tax": None,
"tax": None, },
}, "user": {"username": "Dave", "full_name": None},
"user": {"username": "Dave", "full_name": None}, }
},
),
( def test_post_body_no_data(client: TestClient):
"/items/5", response = client.put("/items/5", json=None)
None, assert response.status_code == 422
422, assert response.json() == IsDict(
{ {
"detail": [ "detail": [
{ {
"loc": ["body", "item"], "type": "missing",
"msg": "field required", "loc": ["body", "item"],
"type": "value_error.missing", "msg": "Field required",
}, "input": None,
{ "url": match_pydantic_error_url("missing"),
"loc": ["body", "user"], },
"msg": "field required", {
"type": "value_error.missing", "type": "missing",
}, "loc": ["body", "user"],
{ "msg": "Field required",
"loc": ["body", "importance"], "input": None,
"msg": "field required", "url": match_pydantic_error_url("missing"),
"type": "value_error.missing", },
}, {
] "type": "missing",
}, "loc": ["body", "importance"],
), "msg": "Field required",
( "input": None,
"/items/5", "url": match_pydantic_error_url("missing"),
[], },
422, ]
{ }
"detail": [ ) | IsDict(
{ # TODO: remove when deprecating Pydantic v1
"loc": ["body", "item"], {
"msg": "field required", "detail": [
"type": "value_error.missing", {
}, "loc": ["body", "item"],
{ "msg": "field required",
"loc": ["body", "user"], "type": "value_error.missing",
"msg": "field required", },
"type": "value_error.missing", {
}, "loc": ["body", "user"],
{ "msg": "field required",
"loc": ["body", "importance"], "type": "value_error.missing",
"msg": "field required", },
"type": "value_error.missing", {
}, "loc": ["body", "importance"],
] "msg": "field required",
}, "type": "value_error.missing",
), },
], ]
) }
def test_post_body(path, body, expected_status, expected_response): )
response = client.put(path, json=body)
assert response.status_code == expected_status
assert response.json() == expected_response
def test_openapi_schema(): def test_post_body_empty_list(client: TestClient):
response = client.put("/items/5", json=[])
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "item"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["body", "user"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["body", "importance"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "item"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "user"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "importance"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
assert response.json() == { assert response.json() == {
@ -142,9 +197,27 @@ def test_openapi_schema():
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"description": IsDict(
{
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Description", "type": "string"}
),
"price": {"title": "Price", "type": "number"}, "price": {"title": "Price", "type": "number"},
"description": {"title": "Description", "type": "string"}, "tax": IsDict(
"tax": {"title": "Tax", "type": "number"}, {
"title": "Tax",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Tax", "type": "number"}
),
}, },
}, },
"User": { "User": {
@ -153,7 +226,16 @@ def test_openapi_schema():
"type": "object", "type": "object",
"properties": { "properties": {
"username": {"title": "Username", "type": "string"}, "username": {"title": "Username", "type": "string"},
"full_name": {"title": "Full Name", "type": "string"}, "full_name": IsDict(
{
"title": "Full Name",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Full Name", "type": "string"}
),
}, },
}, },
"Body_update_item_items__item_id__put": { "Body_update_item_items__item_id__put": {

242
tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py

@ -1,5 +1,7 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py310 from ...utils import needs_py310
@ -12,85 +14,136 @@ def get_client():
return client return client
# Test required and embedded body parameters with no bodies sent
@needs_py310 @needs_py310
@pytest.mark.parametrize( def test_post_body_valid(client: TestClient):
"path,body,expected_status,expected_response", response = client.put(
[ "/items/5",
( json={
"/items/5", "importance": 2,
{ "item": {"name": "Foo", "price": 50.5},
"importance": 2, "user": {"username": "Dave"},
"item": {"name": "Foo", "price": 50.5}, },
"user": {"username": "Dave"}, )
}, assert response.status_code == 200
200, assert response.json() == {
{ "item_id": 5,
"item_id": 5, "importance": 2,
"importance": 2, "item": {
"item": { "name": "Foo",
"name": "Foo", "price": 50.5,
"price": 50.5, "description": None,
"description": None, "tax": None,
"tax": None, },
}, "user": {"username": "Dave", "full_name": None},
"user": {"username": "Dave", "full_name": None}, }
},
),
( @needs_py310
"/items/5", def test_post_body_no_data(client: TestClient):
None, response = client.put("/items/5", json=None)
422, assert response.status_code == 422
{ assert response.json() == IsDict(
"detail": [ {
{ "detail": [
"loc": ["body", "item"], {
"msg": "field required", "type": "missing",
"type": "value_error.missing", "loc": ["body", "item"],
}, "msg": "Field required",
{ "input": None,
"loc": ["body", "user"], "url": match_pydantic_error_url("missing"),
"msg": "field required", },
"type": "value_error.missing", {
}, "type": "missing",
{ "loc": ["body", "user"],
"loc": ["body", "importance"], "msg": "Field required",
"msg": "field required", "input": None,
"type": "value_error.missing", "url": match_pydantic_error_url("missing"),
}, },
] {
}, "type": "missing",
), "loc": ["body", "importance"],
( "msg": "Field required",
"/items/5", "input": None,
[], "url": match_pydantic_error_url("missing"),
422, },
{ ]
"detail": [ }
{ ) | IsDict(
"loc": ["body", "item"], # TODO: remove when deprecating Pydantic v1
"msg": "field required", {
"type": "value_error.missing", "detail": [
}, {
{ "loc": ["body", "item"],
"loc": ["body", "user"], "msg": "field required",
"msg": "field required", "type": "value_error.missing",
"type": "value_error.missing", },
}, {
{ "loc": ["body", "user"],
"loc": ["body", "importance"], "msg": "field required",
"msg": "field required", "type": "value_error.missing",
"type": "value_error.missing", },
}, {
] "loc": ["body", "importance"],
}, "msg": "field required",
), "type": "value_error.missing",
], },
) ]
def test_post_body(path, body, expected_status, expected_response, client: TestClient): }
response = client.put(path, json=body) )
assert response.status_code == expected_status
assert response.json() == expected_response
@needs_py310
def test_post_body_empty_list(client: TestClient):
response = client.put("/items/5", json=[])
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "item"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["body", "user"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["body", "importance"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "item"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "user"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "importance"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
@needs_py310 @needs_py310
@ -150,9 +203,27 @@ def test_openapi_schema(client: TestClient):
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"description": IsDict(
{
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Description", "type": "string"}
),
"price": {"title": "Price", "type": "number"}, "price": {"title": "Price", "type": "number"},
"description": {"title": "Description", "type": "string"}, "tax": IsDict(
"tax": {"title": "Tax", "type": "number"}, {
"title": "Tax",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Tax", "type": "number"}
),
}, },
}, },
"User": { "User": {
@ -161,7 +232,16 @@ def test_openapi_schema(client: TestClient):
"type": "object", "type": "object",
"properties": { "properties": {
"username": {"title": "Username", "type": "string"}, "username": {"title": "Username", "type": "string"},
"full_name": {"title": "Full Name", "type": "string"}, "full_name": IsDict(
{
"title": "Full Name",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Full Name", "type": "string"}
),
}, },
}, },
"Body_update_item_items__item_id__put": { "Body_update_item_items__item_id__put": {

242
tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py

@ -1,5 +1,7 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py39 from ...utils import needs_py39
@ -12,85 +14,136 @@ def get_client():
return client return client
# Test required and embedded body parameters with no bodies sent
@needs_py39 @needs_py39
@pytest.mark.parametrize( def test_post_body_valid(client: TestClient):
"path,body,expected_status,expected_response", response = client.put(
[ "/items/5",
( json={
"/items/5", "importance": 2,
{ "item": {"name": "Foo", "price": 50.5},
"importance": 2, "user": {"username": "Dave"},
"item": {"name": "Foo", "price": 50.5}, },
"user": {"username": "Dave"}, )
}, assert response.status_code == 200
200, assert response.json() == {
{ "item_id": 5,
"item_id": 5, "importance": 2,
"importance": 2, "item": {
"item": { "name": "Foo",
"name": "Foo", "price": 50.5,
"price": 50.5, "description": None,
"description": None, "tax": None,
"tax": None, },
}, "user": {"username": "Dave", "full_name": None},
"user": {"username": "Dave", "full_name": None}, }
},
),
( @needs_py39
"/items/5", def test_post_body_no_data(client: TestClient):
None, response = client.put("/items/5", json=None)
422, assert response.status_code == 422
{ assert response.json() == IsDict(
"detail": [ {
{ "detail": [
"loc": ["body", "item"], {
"msg": "field required", "type": "missing",
"type": "value_error.missing", "loc": ["body", "item"],
}, "msg": "Field required",
{ "input": None,
"loc": ["body", "user"], "url": match_pydantic_error_url("missing"),
"msg": "field required", },
"type": "value_error.missing", {
}, "type": "missing",
{ "loc": ["body", "user"],
"loc": ["body", "importance"], "msg": "Field required",
"msg": "field required", "input": None,
"type": "value_error.missing", "url": match_pydantic_error_url("missing"),
}, },
] {
}, "type": "missing",
), "loc": ["body", "importance"],
( "msg": "Field required",
"/items/5", "input": None,
[], "url": match_pydantic_error_url("missing"),
422, },
{ ]
"detail": [ }
{ ) | IsDict(
"loc": ["body", "item"], # TODO: remove when deprecating Pydantic v1
"msg": "field required", {
"type": "value_error.missing", "detail": [
}, {
{ "loc": ["body", "item"],
"loc": ["body", "user"], "msg": "field required",
"msg": "field required", "type": "value_error.missing",
"type": "value_error.missing", },
}, {
{ "loc": ["body", "user"],
"loc": ["body", "importance"], "msg": "field required",
"msg": "field required", "type": "value_error.missing",
"type": "value_error.missing", },
}, {
] "loc": ["body", "importance"],
}, "msg": "field required",
), "type": "value_error.missing",
], },
) ]
def test_post_body(path, body, expected_status, expected_response, client: TestClient): }
response = client.put(path, json=body) )
assert response.status_code == expected_status
assert response.json() == expected_response
@needs_py39
def test_post_body_empty_list(client: TestClient):
response = client.put("/items/5", json=[])
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "item"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["body", "user"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["body", "importance"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "item"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "user"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "importance"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
@needs_py39 @needs_py39
@ -150,9 +203,27 @@ def test_openapi_schema(client: TestClient):
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"description": IsDict(
{
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Description", "type": "string"}
),
"price": {"title": "Price", "type": "number"}, "price": {"title": "Price", "type": "number"},
"description": {"title": "Description", "type": "string"}, "tax": IsDict(
"tax": {"title": "Tax", "type": "number"}, {
"title": "Tax",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Tax", "type": "number"}
),
}, },
}, },
"User": { "User": {
@ -161,7 +232,16 @@ def test_openapi_schema(client: TestClient):
"type": "object", "type": "object",
"properties": { "properties": {
"username": {"title": "Username", "type": "string"}, "username": {"title": "Username", "type": "string"},
"full_name": {"title": "Full Name", "type": "string"}, "full_name": IsDict(
{
"title": "Full Name",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Full Name", "type": "string"}
),
}, },
}, },
"Body_update_item_items__item_id__put": { "Body_update_item_items__item_id__put": {

242
tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py

@ -1,5 +1,7 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py310 from ...utils import needs_py310
@ -12,85 +14,136 @@ def get_client():
return client return client
# Test required and embedded body parameters with no bodies sent
@needs_py310 @needs_py310
@pytest.mark.parametrize( def test_post_body_valid(client: TestClient):
"path,body,expected_status,expected_response", response = client.put(
[ "/items/5",
( json={
"/items/5", "importance": 2,
{ "item": {"name": "Foo", "price": 50.5},
"importance": 2, "user": {"username": "Dave"},
"item": {"name": "Foo", "price": 50.5}, },
"user": {"username": "Dave"}, )
}, assert response.status_code == 200
200, assert response.json() == {
{ "item_id": 5,
"item_id": 5, "importance": 2,
"importance": 2, "item": {
"item": { "name": "Foo",
"name": "Foo", "price": 50.5,
"price": 50.5, "description": None,
"description": None, "tax": None,
"tax": None, },
}, "user": {"username": "Dave", "full_name": None},
"user": {"username": "Dave", "full_name": None}, }
},
),
( @needs_py310
"/items/5", def test_post_body_no_data(client: TestClient):
None, response = client.put("/items/5", json=None)
422, assert response.status_code == 422
{ assert response.json() == IsDict(
"detail": [ {
{ "detail": [
"loc": ["body", "item"], {
"msg": "field required", "type": "missing",
"type": "value_error.missing", "loc": ["body", "item"],
}, "msg": "Field required",
{ "input": None,
"loc": ["body", "user"], "url": match_pydantic_error_url("missing"),
"msg": "field required", },
"type": "value_error.missing", {
}, "type": "missing",
{ "loc": ["body", "user"],
"loc": ["body", "importance"], "msg": "Field required",
"msg": "field required", "input": None,
"type": "value_error.missing", "url": match_pydantic_error_url("missing"),
}, },
] {
}, "type": "missing",
), "loc": ["body", "importance"],
( "msg": "Field required",
"/items/5", "input": None,
[], "url": match_pydantic_error_url("missing"),
422, },
{ ]
"detail": [ }
{ ) | IsDict(
"loc": ["body", "item"], # TODO: remove when deprecating Pydantic v1
"msg": "field required", {
"type": "value_error.missing", "detail": [
}, {
{ "loc": ["body", "item"],
"loc": ["body", "user"], "msg": "field required",
"msg": "field required", "type": "value_error.missing",
"type": "value_error.missing", },
}, {
{ "loc": ["body", "user"],
"loc": ["body", "importance"], "msg": "field required",
"msg": "field required", "type": "value_error.missing",
"type": "value_error.missing", },
}, {
] "loc": ["body", "importance"],
}, "msg": "field required",
), "type": "value_error.missing",
], },
) ]
def test_post_body(path, body, expected_status, expected_response, client: TestClient): }
response = client.put(path, json=body) )
assert response.status_code == expected_status
assert response.json() == expected_response
@needs_py310
def test_post_body_empty_list(client: TestClient):
response = client.put("/items/5", json=[])
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "item"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["body", "user"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["body", "importance"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "item"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "user"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "importance"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
@needs_py310 @needs_py310
@ -150,9 +203,27 @@ def test_openapi_schema(client: TestClient):
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"description": IsDict(
{
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Description", "type": "string"}
),
"price": {"title": "Price", "type": "number"}, "price": {"title": "Price", "type": "number"},
"description": {"title": "Description", "type": "string"}, "tax": IsDict(
"tax": {"title": "Tax", "type": "number"}, {
"title": "Tax",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Tax", "type": "number"}
),
}, },
}, },
"User": { "User": {
@ -161,7 +232,16 @@ def test_openapi_schema(client: TestClient):
"type": "object", "type": "object",
"properties": { "properties": {
"username": {"title": "Username", "type": "string"}, "username": {"title": "Username", "type": "string"},
"full_name": {"title": "Full Name", "type": "string"}, "full_name": IsDict(
{
"title": "Full Name",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Full Name", "type": "string"}
),
}, },
}, },
"Body_update_item_items__item_id__put": { "Body_update_item_items__item_id__put": {

50
tests/test_tutorial/test_body_nested_models/test_tutorial009.py

@ -1,33 +1,55 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from docs_src.body_nested_models.tutorial009 import app
client = TestClient(app) @pytest.fixture(name="client")
def get_client():
from docs_src.body_nested_models.tutorial009 import app
client = TestClient(app)
return client
def test_post_body():
def test_post_body(client: TestClient):
data = {"2": 2.2, "3": 3.3} data = {"2": 2.2, "3": 3.3}
response = client.post("/index-weights/", json=data) response = client.post("/index-weights/", json=data)
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
assert response.json() == data assert response.json() == data
def test_post_invalid_body(): def test_post_invalid_body(client: TestClient):
data = {"foo": 2.2, "3": 3.3} data = {"foo": 2.2, "3": 3.3}
response = client.post("/index-weights/", json=data) response = client.post("/index-weights/", json=data)
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
assert response.json() == { assert response.json() == IsDict(
"detail": [ {
{ "detail": [
"loc": ["body", "__key__"], {
"msg": "value is not a valid integer", "type": "int_parsing",
"type": "type_error.integer", "loc": ["body", "foo", "[key]"],
} "msg": "Input should be a valid integer, unable to parse string as an integer",
] "input": "foo",
} "url": match_pydantic_error_url("int_parsing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "__key__"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
def test_openapi_schema(): def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
assert response.json() == { assert response.json() == {

35
tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py

@ -1,5 +1,7 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py39 from ...utils import needs_py39
@ -25,15 +27,30 @@ def test_post_invalid_body(client: TestClient):
data = {"foo": 2.2, "3": 3.3} data = {"foo": 2.2, "3": 3.3}
response = client.post("/index-weights/", json=data) response = client.post("/index-weights/", json=data)
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
assert response.json() == { assert response.json() == IsDict(
"detail": [ {
{ "detail": [
"loc": ["body", "__key__"], {
"msg": "value is not a valid integer", "type": "int_parsing",
"type": "type_error.integer", "loc": ["body", "foo", "[key]"],
} "msg": "Input should be a valid integer, unable to parse string as an integer",
] "input": "foo",
} "url": match_pydantic_error_url("int_parsing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "__key__"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
@needs_py39 @needs_py39

49
tests/test_tutorial/test_body_updates/test_tutorial001.py

@ -1,11 +1,17 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from docs_src.body_updates.tutorial001 import app
client = TestClient(app) @pytest.fixture(name="client")
def get_client():
from docs_src.body_updates.tutorial001 import app
client = TestClient(app)
return client
def test_get():
def test_get(client: TestClient):
response = client.get("/items/baz") response = client.get("/items/baz")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
assert response.json() == { assert response.json() == {
@ -17,7 +23,7 @@ def test_get():
} }
def test_put(): def test_put(client: TestClient):
response = client.put( response = client.put(
"/items/bar", json={"name": "Barz", "price": 3, "description": None} "/items/bar", json={"name": "Barz", "price": 3, "description": None}
) )
@ -30,7 +36,7 @@ def test_put():
} }
def test_openapi_schema(): def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
assert response.json() == { assert response.json() == {
@ -118,9 +124,36 @@ def test_openapi_schema():
"title": "Item", "title": "Item",
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": IsDict(
"description": {"title": "Description", "type": "string"}, {
"price": {"title": "Price", "type": "number"}, "title": "Name",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Name", "type": "string"}
),
"description": IsDict(
{
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Description", "type": "string"}
),
"price": IsDict(
{
"title": "Price",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Price", "type": "number"}
),
"tax": {"title": "Tax", "type": "number", "default": 10.5}, "tax": {"title": "Tax", "type": "number", "default": 10.5},
"tags": { "tags": {
"title": "Tags", "title": "Tags",

34
tests/test_tutorial/test_body_updates/test_tutorial001_py310.py

@ -1,4 +1,5 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from ...utils import needs_py310 from ...utils import needs_py310
@ -128,9 +129,36 @@ def test_openapi_schema(client: TestClient):
"title": "Item", "title": "Item",
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": IsDict(
"description": {"title": "Description", "type": "string"}, {
"price": {"title": "Price", "type": "number"}, "title": "Name",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Name", "type": "string"}
),
"description": IsDict(
{
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Description", "type": "string"}
),
"price": IsDict(
{
"title": "Price",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Price", "type": "number"}
),
"tax": {"title": "Tax", "type": "number", "default": 10.5}, "tax": {"title": "Tax", "type": "number", "default": 10.5},
"tags": { "tags": {
"title": "Tags", "title": "Tags",

34
tests/test_tutorial/test_body_updates/test_tutorial001_py39.py

@ -1,4 +1,5 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from ...utils import needs_py39 from ...utils import needs_py39
@ -128,9 +129,36 @@ def test_openapi_schema(client: TestClient):
"title": "Item", "title": "Item",
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": IsDict(
"description": {"title": "Description", "type": "string"}, {
"price": {"title": "Price", "type": "number"}, "title": "Name",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Name", "type": "string"}
),
"description": IsDict(
{
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Description", "type": "string"}
),
"price": IsDict(
{
"title": "Price",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Price", "type": "number"}
),
"tax": {"title": "Tax", "type": "number", "default": 10.5}, "tax": {"title": "Tax", "type": "number", "default": 10.5},
"tags": { "tags": {
"title": "Tags", "title": "Tags",

26
tests/test_tutorial/test_conditional_openapi/test_tutorial001.py

@ -2,13 +2,24 @@ import importlib
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from docs_src.conditional_openapi import tutorial001 from ...utils import needs_pydanticv1
def test_disable_openapi(monkeypatch): def get_client() -> TestClient:
monkeypatch.setenv("OPENAPI_URL", "") from docs_src.conditional_openapi import tutorial001
importlib.reload(tutorial001) importlib.reload(tutorial001)
client = TestClient(tutorial001.app) client = TestClient(tutorial001.app)
return client
# TODO: pv2 add version with Pydantic v2
@needs_pydanticv1
def test_disable_openapi(monkeypatch):
monkeypatch.setenv("OPENAPI_URL", "")
# Load the client after setting the env var
client = get_client()
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 404, response.text assert response.status_code == 404, response.text
response = client.get("/docs") response = client.get("/docs")
@ -17,16 +28,19 @@ def test_disable_openapi(monkeypatch):
assert response.status_code == 404, response.text assert response.status_code == 404, response.text
# TODO: pv2 add version with Pydantic v2
@needs_pydanticv1
def test_root(): def test_root():
client = TestClient(tutorial001.app) client = get_client()
response = client.get("/") response = client.get("/")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"message": "Hello World"} assert response.json() == {"message": "Hello World"}
# TODO: pv2 add version with Pydantic v2
@needs_pydanticv1
def test_default_openapi(): def test_default_openapi():
importlib.reload(tutorial001) client = get_client()
client = TestClient(tutorial001.app)
response = client.get("/docs") response = client.get("/docs")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
response = client.get("/redoc") response = client.get("/redoc")

12
tests/test_tutorial/test_cookie_params/test_tutorial001.py

@ -1,4 +1,5 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from docs_src.cookie_params.tutorial001 import app from docs_src.cookie_params.tutorial001 import app
@ -56,7 +57,16 @@ def test_openapi_schema():
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Ads Id", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Ads Id",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Ads Id", "type": "string"}
),
"name": "ads_id", "name": "ads_id",
"in": "cookie", "in": "cookie",
} }

12
tests/test_tutorial/test_cookie_params/test_tutorial001_an.py

@ -1,4 +1,5 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from docs_src.cookie_params.tutorial001_an import app from docs_src.cookie_params.tutorial001_an import app
@ -56,7 +57,16 @@ def test_openapi_schema():
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Ads Id", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Ads Id",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Ads Id", "type": "string"}
),
"name": "ads_id", "name": "ads_id",
"in": "cookie", "in": "cookie",
} }

12
tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py

@ -1,4 +1,5 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from ...utils import needs_py310 from ...utils import needs_py310
@ -62,7 +63,16 @@ def test_openapi_schema():
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Ads Id", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Ads Id",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Ads Id", "type": "string"}
),
"name": "ads_id", "name": "ads_id",
"in": "cookie", "in": "cookie",
} }

12
tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py

@ -1,4 +1,5 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from ...utils import needs_py39 from ...utils import needs_py39
@ -62,7 +63,16 @@ def test_openapi_schema():
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Ads Id", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Ads Id",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Ads Id", "type": "string"}
),
"name": "ads_id", "name": "ads_id",
"in": "cookie", "in": "cookie",
} }

12
tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py

@ -1,4 +1,5 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from ...utils import needs_py310 from ...utils import needs_py310
@ -62,7 +63,16 @@ def test_openapi_schema():
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Ads Id", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Ads Id",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Ads Id", "type": "string"}
),
"name": "ads_id", "name": "ads_id",
"in": "cookie", "in": "cookie",
} }

43
tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py

@ -1,4 +1,6 @@
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from docs_src.custom_request_and_route.tutorial002 import app from docs_src.custom_request_and_route.tutorial002 import app
@ -12,16 +14,33 @@ def test_endpoint_works():
def test_exception_handler_body_access(): def test_exception_handler_body_access():
response = client.post("/", json={"numbers": [1, 2, 3]}) response = client.post("/", json={"numbers": [1, 2, 3]})
assert response.json() == IsDict(
assert response.json() == { {
"detail": { "detail": {
"body": '{"numbers": [1, 2, 3]}', "errors": [
"errors": [ {
{ "type": "list_type",
"loc": ["body"], "loc": ["body"],
"msg": "value is not a valid list", "msg": "Input should be a valid list",
"type": "type_error.list", "input": {"numbers": [1, 2, 3]},
} "url": match_pydantic_error_url("list_type"),
], }
],
"body": '{"numbers": [1, 2, 3]}',
}
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": {
"body": '{"numbers": [1, 2, 3]}',
"errors": [
{
"loc": ["body"],
"msg": "value is not a valid list",
"type": "type_error.list",
}
],
}
} }
} )

57
tests/test_tutorial/test_dataclasses/test_tutorial001.py

@ -1,4 +1,6 @@
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from docs_src.dataclasses.tutorial001 import app from docs_src.dataclasses.tutorial001 import app
@ -19,15 +21,30 @@ def test_post_item():
def test_post_invalid_item(): def test_post_invalid_item():
response = client.post("/items/", json={"name": "Foo", "price": "invalid price"}) response = client.post("/items/", json={"name": "Foo", "price": "invalid price"})
assert response.status_code == 422 assert response.status_code == 422
assert response.json() == { assert response.json() == IsDict(
"detail": [ {
{ "detail": [
"loc": ["body", "price"], {
"msg": "value is not a valid float", "type": "float_parsing",
"type": "type_error.float", "loc": ["body", "price"],
} "msg": "Input should be a valid number, unable to parse string as an number",
] "input": "invalid price",
} "url": match_pydantic_error_url("float_parsing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "price"],
"msg": "value is not a valid float",
"type": "type_error.float",
}
]
}
)
def test_openapi_schema(): def test_openapi_schema():
@ -88,8 +105,26 @@ def test_openapi_schema():
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"}, "price": {"title": "Price", "type": "number"},
"description": {"title": "Description", "type": "string"}, "description": IsDict(
"tax": {"title": "Tax", "type": "number"}, {
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Description", "type": "string"}
),
"tax": IsDict(
{
"title": "Tax",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Tax", "type": "number"}
),
}, },
}, },
"ValidationError": { "ValidationError": {

47
tests/test_tutorial/test_dataclasses/test_tutorial002.py

@ -1,3 +1,4 @@
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from docs_src.dataclasses.tutorial002 import app from docs_src.dataclasses.tutorial002 import app
@ -20,8 +21,7 @@ def test_get_item():
def test_openapi_schema(): def test_openapi_schema():
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200 assert response.status_code == 200
data = response.json() assert response.json() == {
assert data == {
"openapi": "3.0.2", "openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"}, "info": {"title": "FastAPI", "version": "0.1.0"},
"paths": { "paths": {
@ -51,13 +51,42 @@ def test_openapi_schema():
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"}, "price": {"title": "Price", "type": "number"},
"tags": { "tags": IsDict(
"title": "Tags", {
"type": "array", "title": "Tags",
"items": {"type": "string"}, "type": "array",
}, "items": {"type": "string"},
"description": {"title": "Description", "type": "string"}, "default": [],
"tax": {"title": "Tax", "type": "number"}, }
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "Tags",
"type": "array",
"items": {"type": "string"},
}
),
"description": IsDict(
{
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Description", "type": "string"}
),
"tax": IsDict(
{
"title": "Tax",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Tax", "type": "number"}
),
}, },
} }
} }

33
tests/test_tutorial/test_dataclasses/test_tutorial003.py

@ -1,3 +1,4 @@
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from docs_src.dataclasses.tutorial003 import app from docs_src.dataclasses.tutorial003 import app
@ -135,11 +136,22 @@ def test_openapi_schema():
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"items": { "items": IsDict(
"title": "Items", {
"type": "array", "title": "Items",
"items": {"$ref": "#/components/schemas/Item"}, "type": "array",
}, "items": {"$ref": "#/components/schemas/Item"},
"default": [],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "Items",
"type": "array",
"items": {"$ref": "#/components/schemas/Item"},
}
),
}, },
}, },
"HTTPValidationError": { "HTTPValidationError": {
@ -159,7 +171,16 @@ def test_openapi_schema():
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"description": {"title": "Description", "type": "string"}, "description": IsDict(
{
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Description", "type": "string"}
),
}, },
}, },
"ValidationError": { "ValidationError": {

23
tests/test_tutorial/test_dependencies/test_tutorial001.py

@ -1,4 +1,5 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from docs_src.dependencies.tutorial001 import app from docs_src.dependencies.tutorial001 import app
@ -52,7 +53,16 @@ def test_openapi_schema():
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Q", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Q",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Q", "type": "string"}
),
"name": "q", "name": "q",
"in": "query", "in": "query",
}, },
@ -102,7 +112,16 @@ def test_openapi_schema():
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Q", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Q",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Q", "type": "string"}
),
"name": "q", "name": "q",
"in": "query", "in": "query",
}, },

23
tests/test_tutorial/test_dependencies/test_tutorial001_an.py

@ -1,4 +1,5 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from docs_src.dependencies.tutorial001_an import app from docs_src.dependencies.tutorial001_an import app
@ -52,7 +53,16 @@ def test_openapi_schema():
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Q", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Q",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Q", "type": "string"}
),
"name": "q", "name": "q",
"in": "query", "in": "query",
}, },
@ -102,7 +112,16 @@ def test_openapi_schema():
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Q", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Q",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Q", "type": "string"}
),
"name": "q", "name": "q",
"in": "query", "in": "query",
}, },

23
tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py

@ -1,4 +1,5 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from ...utils import needs_py310 from ...utils import needs_py310
@ -60,7 +61,16 @@ def test_openapi_schema(client: TestClient):
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Q", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Q",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Q", "type": "string"}
),
"name": "q", "name": "q",
"in": "query", "in": "query",
}, },
@ -110,7 +120,16 @@ def test_openapi_schema(client: TestClient):
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Q", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Q",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Q", "type": "string"}
),
"name": "q", "name": "q",
"in": "query", "in": "query",
}, },

23
tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py

@ -1,4 +1,5 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from ...utils import needs_py39 from ...utils import needs_py39
@ -60,7 +61,16 @@ def test_openapi_schema(client: TestClient):
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Q", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Q",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Q", "type": "string"}
),
"name": "q", "name": "q",
"in": "query", "in": "query",
}, },
@ -110,7 +120,16 @@ def test_openapi_schema(client: TestClient):
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Q", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Q",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Q", "type": "string"}
),
"name": "q", "name": "q",
"in": "query", "in": "query",
}, },

23
tests/test_tutorial/test_dependencies/test_tutorial001_py310.py

@ -1,4 +1,5 @@
import pytest import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from ...utils import needs_py310 from ...utils import needs_py310
@ -60,7 +61,16 @@ def test_openapi_schema(client: TestClient):
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Q", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Q",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Q", "type": "string"}
),
"name": "q", "name": "q",
"in": "query", "in": "query",
}, },
@ -110,7 +120,16 @@ def test_openapi_schema(client: TestClient):
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Q", "type": "string"}, "schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Q",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Q", "type": "string"}
),
"name": "q", "name": "q",
"in": "query", "in": "query",
}, },

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save