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):
type = "car"
type: str = "car"
class PlaneItem(BaseItem):
type = "plane"
type: str = "plane"
size: int

4
docs_src/extra_models/tutorial003_py310.py

@ -12,11 +12,11 @@ class BaseItem(BaseModel):
class CarItem(BaseItem):
type = "car"
type: str = "car"
class PlaneItem(BaseItem):
type = "plane"
type: str = "plane"
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.datastructures import Default, DefaultPlaceholder
from fastapi.encoders import DictIntStrAny, SetIntStr
from fastapi.exception_handlers import (
http_exception_handler,
request_validation_exception_handler,
@ -31,7 +30,7 @@ from fastapi.openapi.docs import (
)
from fastapi.openapi.utils import get_openapi
from fastapi.params import Depends
from fastapi.types import DecoratedCallable
from fastapi.types import DecoratedCallable, IncEx
from fastapi.utils import generate_unique_id
from starlette.applications import Starlette
from starlette.datastructures import State
@ -297,8 +296,8 @@ class FastAPI(Starlette):
deprecated: Optional[bool] = None,
methods: Optional[List[str]] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@ -355,8 +354,8 @@ class FastAPI(Starlette):
deprecated: Optional[bool] = None,
methods: Optional[List[str]] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: 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,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: 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,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: 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,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: 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,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: 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,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: 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,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: 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,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: 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,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: 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 Address as Address # noqa: F401
from starlette.datastructures import FormData as FormData # noqa: F401
@ -21,8 +28,28 @@ class UploadFile(StarletteUploadFile):
return v
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
field_schema.update({"type": "string", "format": "binary"})
def _validate(cls, __input_value: Any, _: Any) -> "UploadFile":
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:

2
fastapi/dependencies/models.py

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

205
fastapi/dependencies/utils.py

@ -1,7 +1,6 @@
import dataclasses
import inspect
from contextlib import contextmanager
from copy import copy, deepcopy
from copy import deepcopy
from typing import (
Any,
Callable,
@ -20,6 +19,32 @@ from typing import (
import anyio
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 (
AsyncExitStack,
asynccontextmanager,
@ -31,50 +56,14 @@ from fastapi.security.base import SecurityBase
from fastapi.security.oauth2 import OAuth2, SecurityScopes
from fastapi.security.open_id_connect_url import OpenIdConnect
from fastapi.utils import create_response_field, get_path_param_names
from pydantic import BaseModel, create_model
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 pydantic.fields import FieldInfo
from starlette.background import BackgroundTasks
from starlette.concurrency import run_in_threadpool
from starlette.datastructures import FormData, Headers, QueryParams, UploadFile
from starlette.requests import HTTPConnection, Request
from starlette.responses import Response
from starlette.websockets import WebSocket
from typing_extensions import Annotated
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,
}
from typing_extensions import Annotated, get_args, get_origin
multipart_not_installed_error = (
'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:
signature = inspect.signature(call)
globalns = getattr(call, "__globals__", {})
@ -364,12 +323,11 @@ def analyze_param(
is_path_param: bool,
) -> Tuple[Any, Optional[params.Depends], Optional[ModelField]]:
field_info = None
used_default_field_info = False
depends = None
type_annotation: Any = Any
if (
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)
type_annotation = annotated_args[0]
@ -384,7 +342,9 @@ def analyze_param(
fastapi_annotation = next(iter(fastapi_annotations), None)
if isinstance(fastapi_annotation, FieldInfo):
# 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, (
f"`{field_info.__class__.__name__}` default value cannot be set in"
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}"
)
field_info = value
if PYDANTIC_V2:
field_info.annotation = type_annotation
if depends is not None and depends.dependency is None:
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
# parameter might sometimes be a path parameter and sometimes not. See
# `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:
field_info = params.Query(default=default_value)
used_default_field_info = True
field_info = params.Query(annotation=type_annotation, default=default_value)
field = None
if field_info is not None:
@ -450,8 +417,8 @@ def analyze_param(
and getattr(field_info, "in_", None) is None
):
field_info.in_ = params.ParamTypes.query
annotation = get_annotation_from_field_info(
annotation if annotation is not inspect.Signature.empty else Any,
use_annotation = get_annotation_from_field_info(
type_annotation,
field_info,
param_name,
)
@ -459,19 +426,15 @@ def analyze_param(
alias = param_name.replace("_", "-")
else:
alias = field_info.alias or param_name
field_info.alias = alias
field = create_response_field(
name=param_name,
type_=annotation,
type_=use_annotation,
default=field_info.default,
alias=alias,
required=field_info.default in (Required, Undefined),
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
@ -554,13 +517,13 @@ async def solve_dependencies(
dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None,
) -> Tuple[
Dict[str, Any],
List[ErrorWrapper],
List[Any],
Optional[BackgroundTasks],
Response,
Dict[Tuple[Callable[..., Any], Tuple[str]], Any],
]:
values: Dict[str, Any] = {}
errors: List[ErrorWrapper] = []
errors: List[Any] = []
if response is None:
response = Response()
del response.headers["content-length"]
@ -674,7 +637,7 @@ async def solve_dependencies(
def request_params_to_args(
required_params: Sequence[ModelField],
received_params: Union[Mapping[str, Any], QueryParams, Headers],
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
) -> Tuple[Dict[str, Any], List[Any]]:
values = {}
errors = []
for field in required_params:
@ -688,23 +651,25 @@ def request_params_to_args(
assert isinstance(
field_info, params.Param
), "Params must be subclasses of Param"
loc = (field_info.in_.value, field.alias)
if value is None:
if field.required:
errors.append(
ErrorWrapper(
MissingError(), loc=(field_info.in_.value, field.alias)
)
)
errors.append(get_missing_field_error(loc=loc))
else:
values[field.name] = deepcopy(field.default)
continue
v_, errors_ = field.validate(
value, values, loc=(field_info.in_.value, field.alias)
)
if isinstance(errors_, ErrorWrapper):
v_, errors_ = field.validate(value, values, loc=loc)
if isinstance(errors_, ValidationError):
new_errors = _regenerate_error_with_loc(
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_)
elif isinstance(errors_, list):
errors.extend(errors_)
new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
errors.extend(new_errors)
else:
values[field.name] = v_
return values, errors
@ -713,9 +678,9 @@ def request_params_to_args(
async def request_body_to_args(
required_params: List[ModelField],
received_body: Optional[Union[Dict[str, Any], FormData]],
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
) -> Tuple[Dict[str, Any], List[ValidationError]]:
values = {}
errors = []
errors: List[ValidationError] = []
if required_params:
field = required_params[0]
field_info = field.field_info
@ -733,9 +698,7 @@ async def request_body_to_args(
value: Optional[Any] = None
if received_body is not None:
if (
field.shape in sequence_shapes or field.type_ in sequence_types
) and isinstance(received_body, FormData):
if (is_sequence_field(field)) and isinstance(received_body, FormData):
value = received_body.getlist(field.alias)
else:
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 field.shape in sequence_shapes
and is_sequence_field(field)
and len(value) == 0
)
):
@ -759,16 +722,17 @@ async def request_body_to_args(
continue
if (
isinstance(field_info, params.File)
and lenient_issubclass(field.type_, bytes)
and is_bytes_field(field)
and isinstance(value, UploadFile)
):
value = await value.read()
elif (
field.shape in sequence_shapes
is_bytes_sequence_field(field)
and isinstance(field_info, params.File)
and lenient_issubclass(field.type_, bytes)
and isinstance(value, sequence_types)
and value_is_sequence(value)
):
# For types
assert isinstance(value, sequence_types) # type: ignore[arg-type]
results: List[Union[bytes, str]] = []
async def process_fn(
@ -780,24 +744,19 @@ async def request_body_to_args(
async with anyio.create_task_group() as tg:
for sub_value in value:
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)
if isinstance(errors_, ErrorWrapper):
errors.append(errors_)
elif isinstance(errors_, list):
if isinstance(errors_, list):
errors.extend(errors_)
elif errors_:
errors.append(errors_)
else:
values[field.name] = v_
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]:
flat_dependant = get_flat_dependant(dependant)
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:
setattr(param.field_info, "embed", True) # noqa: B010
model_name = "Body_" + name
BodyModel: Type[BaseModel] = create_model(model_name)
for f in flat_dependant.body_params:
BodyModel.__fields__[f.name] = f
BodyModel = create_body_model(
fields=flat_dependant.body_params, model_name=model_name
)
required = any(True for f in flat_dependant.body_params if f.required)
BodyFieldInfo_kwargs: Dict[str, Any] = {"default": None}
BodyFieldInfo_kwargs: Dict[str, Any] = {
"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):
BodyFieldInfo: Type[params.Body] = params.File
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
from collections import defaultdict
import datetime
from collections import defaultdict, deque
from decimal import Decimal
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 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.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]]
DictIntStrAny = Dict[Union[int, str], Any]
from ._compat import PYDANTIC_V2, MultiHostUrl, Url, _model_dump
# 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(
@ -28,8 +99,8 @@ encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE)
def jsonable_encoder(
obj: Any,
include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
include: Optional[IncEx] = None,
exclude: Optional[IncEx] = None,
by_alias: bool = True,
exclude_unset: bool = False,
exclude_defaults: bool = False,
@ -50,10 +121,15 @@ def jsonable_encoder(
if exclude is not None and not isinstance(exclude, (set, dict)):
exclude = set(exclude)
if isinstance(obj, BaseModel):
encoder = getattr(obj.__config__, "json_encoders", {})
if custom_encoder:
encoder.update(custom_encoder)
obj_dict = obj.dict(
# TODO: remove when deprecating Pydantic v1
encoders: Dict[Any, Any] = {}
if not PYDANTIC_V2:
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,
exclude=exclude,
by_alias=by_alias,
@ -67,7 +143,8 @@ def jsonable_encoder(
obj_dict,
exclude_none=exclude_none,
exclude_defaults=exclude_defaults,
custom_encoder=encoder,
# TODO: remove when deprecating Pydantic v1
custom_encoder=encoders,
sqlalchemy_safe=sqlalchemy_safe,
)
if dataclasses.is_dataclass(obj):

28
fastapi/exceptions.py

@ -1,7 +1,6 @@
from typing import Any, Dict, Optional, Sequence, Type
from pydantic import BaseModel, ValidationError, create_model
from pydantic.error_wrappers import ErrorList
from pydantic import BaseModel, create_model
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.exceptions import WebSocketException as WebSocketException # noqa: F401
@ -26,12 +25,25 @@ class FastAPIError(RuntimeError):
"""
class RequestValidationError(ValidationError):
def __init__(self, errors: Sequence[ErrorList], *, body: Any = None) -> None:
class ValidationException(Exception):
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
super().__init__(errors, RequestErrorModel)
class WebSocketRequestValidationError(ValidationError):
def __init__(self, errors: Sequence[ErrorList]) -> None:
super().__init__(errors, WebSocketErrorModel)
class WebSocketRequestValidationError(ValidationException):
pass
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"}
REF_PREFIX = "#/components/schemas/"
REF_TEMPLATE = "#/components/schemas/{model}"

216
fastapi/openapi/models.py

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

101
fastapi/openapi/utils.py

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

16
fastapi/param_functions.py

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

65
fastapi/params.py

@ -1,7 +1,9 @@
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):
@ -18,6 +20,7 @@ class Param(FieldInfo):
self,
default: Any = Undefined,
*,
annotation: Optional[Type[Any]] = None,
alias: Optional[str] = None,
title: Optional[str] = None,
description: Optional[str] = None,
@ -27,6 +30,7 @@ class Param(FieldInfo):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@ -36,9 +40,8 @@ class Param(FieldInfo):
):
self.deprecated = deprecated
self.example = example
self.examples = examples
self.include_in_schema = include_in_schema
super().__init__(
kwargs = dict(
default=default,
alias=alias,
title=title,
@ -49,9 +52,19 @@ class Param(FieldInfo):
le=le,
min_length=min_length,
max_length=max_length,
regex=regex,
**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:
return f"{self.__class__.__name__}({self.default})"
@ -64,6 +77,7 @@ class Path(Param):
self,
default: Any = ...,
*,
annotation: Optional[Type[Any]] = None,
alias: Optional[str] = None,
title: Optional[str] = None,
description: Optional[str] = None,
@ -73,6 +87,7 @@ class Path(Param):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@ -84,6 +99,7 @@ class Path(Param):
self.in_ = self.in_
super().__init__(
default=default,
annotation=annotation,
alias=alias,
title=title,
description=description,
@ -93,6 +109,7 @@ class Path(Param):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
deprecated=deprecated,
example=example,
@ -109,6 +126,7 @@ class Query(Param):
self,
default: Any = Undefined,
*,
annotation: Optional[Type[Any]] = None,
alias: Optional[str] = None,
title: Optional[str] = None,
description: Optional[str] = None,
@ -118,6 +136,7 @@ class Query(Param):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@ -127,6 +146,7 @@ class Query(Param):
):
super().__init__(
default=default,
annotation=annotation,
alias=alias,
title=title,
description=description,
@ -136,6 +156,7 @@ class Query(Param):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
deprecated=deprecated,
example=example,
@ -152,6 +173,7 @@ class Header(Param):
self,
default: Any = Undefined,
*,
annotation: Optional[Type[Any]] = None,
alias: Optional[str] = None,
convert_underscores: bool = True,
title: Optional[str] = None,
@ -162,6 +184,7 @@ class Header(Param):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@ -172,6 +195,7 @@ class Header(Param):
self.convert_underscores = convert_underscores
super().__init__(
default=default,
annotation=annotation,
alias=alias,
title=title,
description=description,
@ -181,6 +205,7 @@ class Header(Param):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
deprecated=deprecated,
example=example,
@ -197,6 +222,7 @@ class Cookie(Param):
self,
default: Any = Undefined,
*,
annotation: Optional[Type[Any]] = None,
alias: Optional[str] = None,
title: Optional[str] = None,
description: Optional[str] = None,
@ -206,6 +232,7 @@ class Cookie(Param):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@ -215,6 +242,7 @@ class Cookie(Param):
):
super().__init__(
default=default,
annotation=annotation,
alias=alias,
title=title,
description=description,
@ -224,6 +252,7 @@ class Cookie(Param):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
deprecated=deprecated,
example=example,
@ -238,6 +267,7 @@ class Body(FieldInfo):
self,
default: Any = Undefined,
*,
annotation: Optional[Type[Any]] = None,
embed: bool = False,
media_type: str = "application/json",
alias: Optional[str] = None,
@ -249,6 +279,7 @@ class Body(FieldInfo):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@ -257,8 +288,7 @@ class Body(FieldInfo):
self.embed = embed
self.media_type = media_type
self.example = example
self.examples = examples
super().__init__(
kwargs = dict(
default=default,
alias=alias,
title=title,
@ -269,9 +299,20 @@ class Body(FieldInfo):
le=le,
min_length=min_length,
max_length=max_length,
regex=regex,
**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:
return f"{self.__class__.__name__}({self.default})"
@ -282,6 +323,7 @@ class Form(Body):
self,
default: Any = Undefined,
*,
annotation: Optional[Type[Any]] = None,
media_type: str = "application/x-www-form-urlencoded",
alias: Optional[str] = None,
title: Optional[str] = None,
@ -292,6 +334,7 @@ class Form(Body):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@ -299,6 +342,7 @@ class Form(Body):
):
super().__init__(
default=default,
annotation=annotation,
embed=True,
media_type=media_type,
alias=alias,
@ -310,6 +354,7 @@ class Form(Body):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
example=example,
examples=examples,
@ -322,6 +367,7 @@ class File(Form):
self,
default: Any = Undefined,
*,
annotation: Optional[Type[Any]] = None,
media_type: str = "multipart/form-data",
alias: Optional[str] = None,
title: Optional[str] = None,
@ -332,6 +378,7 @@ class File(Form):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
@ -339,6 +386,7 @@ class File(Form):
):
super().__init__(
default=default,
annotation=annotation,
media_type=media_type,
alias=alias,
title=title,
@ -349,6 +397,7 @@ class File(Form):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
example=example,
examples=examples,

125
fastapi/routing.py

@ -20,6 +20,14 @@ from typing import (
)
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.dependencies.models import Dependant
from fastapi.dependencies.utils import (
@ -29,13 +37,14 @@ from fastapi.dependencies.utils import (
get_typed_return_annotation,
solve_dependencies,
)
from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import (
FastAPIError,
RequestValidationError,
ResponseValidationError,
WebSocketRequestValidationError,
)
from fastapi.types import DecoratedCallable
from fastapi.types import DecoratedCallable, IncEx
from fastapi.utils import (
create_cloned_field,
create_response_field,
@ -44,9 +53,6 @@ from fastapi.utils import (
is_body_allowed_for_status_code,
)
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.concurrency import run_in_threadpool
from starlette.exceptions import HTTPException
@ -73,14 +79,15 @@ def _prepare_response_content(
exclude_none: bool = False,
) -> Any:
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:
# Let from_orm extract the data from this model instead of converting
# it now to a dict.
# Otherwise there's no way to extract lazy data that requires attribute
# access instead of dict iteration, e.g. lazy relationships.
return res
return res.dict(
return _model_dump(
res,
by_alias=True,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
@ -115,8 +122,8 @@ async def serialize_response(
*,
field: Optional[ModelField] = None,
response_content: Any,
include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
include: Optional[IncEx] = None,
exclude: Optional[IncEx] = None,
by_alias: bool = True,
exclude_unset: bool = False,
exclude_defaults: bool = False,
@ -125,24 +132,40 @@ async def serialize_response(
) -> Any:
if field:
errors = []
response_content = _prepare_response_content(
response_content,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
if not hasattr(field, "serialize"):
# pydantic v1
response_content = _prepare_response_content(
response_content,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
if is_coroutine:
value, errors_ = field.validate(response_content, {}, loc=("response",))
else:
value, errors_ = await run_in_threadpool(
field.validate, response_content, {}, loc=("response",)
)
if isinstance(errors_, ErrorWrapper):
errors.append(errors_)
elif isinstance(errors_, list):
if isinstance(errors_, list):
errors.extend(errors_)
elif errors_:
errors.append(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(
value,
include=include,
@ -175,8 +198,8 @@ def get_request_handler(
status_code: Optional[int] = None,
response_class: Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse),
response_field: Optional[ModelField] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@ -220,7 +243,16 @@ def get_request_handler(
body = body_bytes
except json.JSONDecodeError as e:
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
except HTTPException:
raise
@ -236,7 +268,7 @@ def get_request_handler(
)
values, errors, background_tasks, sub_response, _ = solved_result
if errors:
raise RequestValidationError(errors, body=body)
raise RequestValidationError(_normalize_errors(errors), body=body)
else:
raw_response = await run_endpoint_function(
dependant=dependant, values=values, is_coroutine=is_coroutine
@ -287,7 +319,7 @@ def get_websocket_app(
)
values, errors, _, _2, _3 = solved_result
if errors:
raise WebSocketRequestValidationError(errors)
raise WebSocketRequestValidationError(_normalize_errors(errors))
assert dependant.call is not None, "dependant.call must be a function"
await dependant.call(**values)
@ -348,8 +380,8 @@ class APIRoute(routing.Route):
name: Optional[str] = None,
methods: Optional[Union[Set[str], List[str]]] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: 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.
# By being a new field, no inheritance will be passed as is. A new model
# will be always created.
# TODO: remove when deprecating Pydantic v1
self.secure_cloned_response_field: Optional[
ModelField
] = create_cloned_field(self.response_field)
@ -569,8 +602,8 @@ class APIRouter(routing.Router):
deprecated: Optional[bool] = None,
methods: Optional[Union[Set[str], List[str]]] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@ -650,8 +683,8 @@ class APIRouter(routing.Router):
deprecated: Optional[bool] = None,
methods: Optional[List[str]] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: 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,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: 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,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: 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,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: 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,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: 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,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: 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,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: 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,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: 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,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: 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
if sys.version_info < (3, 9):
from typing_extensions import Annotated
else:
from typing import Annotated
from fastapi.exceptions import HTTPException
from fastapi.openapi.models import OAuth2 as OAuth2Model
from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
@ -45,12 +51,13 @@ class OAuth2PasswordRequestForm:
def __init__(
self,
grant_type: str = Form(default=None, regex="password"),
username: str = Form(),
password: str = Form(),
scope: str = Form(default=""),
client_id: Optional[str] = Form(default=None),
client_secret: Optional[str] = Form(default=None),
*,
grant_type: Annotated[Union[str, None], Form(pattern="password")] = None,
username: Annotated[str, Form()],
password: Annotated[str, Form()],
scope: Annotated[str, Form()] = "",
client_id: Annotated[Union[str, None], Form()] = None,
client_secret: Annotated[Union[str, None], Form()] = None,
):
self.grant_type = grant_type
self.username = username
@ -95,12 +102,12 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm):
def __init__(
self,
grant_type: str = Form(regex="password"),
username: str = Form(),
password: str = Form(),
scope: str = Form(default=""),
client_id: Optional[str] = Form(default=None),
client_secret: Optional[str] = Form(default=None),
grant_type: Annotated[str, Form(pattern="password")],
username: Annotated[str, Form()],
password: Annotated[str, Form()],
scope: Annotated[str, Form()] = "",
client_id: Annotated[Union[str, None], Form()] = None,
client_secret: Annotated[Union[str, None], Form()] = None,
):
super().__init__(
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])
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 warnings
from dataclasses import is_dataclass
from enum import Enum
from typing import (
TYPE_CHECKING,
Any,
@ -16,13 +15,20 @@ from typing import (
from weakref import WeakKeyDictionary
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.openapi.constants import REF_PREFIX
from pydantic import BaseConfig, BaseModel, create_model
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
from pydantic import BaseModel, create_model
from pydantic.fields import FieldInfo
if TYPE_CHECKING: # pragma: nocover
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})
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]:
return set(re.findall("{(.*?)}", path))
@ -76,8 +64,8 @@ def create_response_field(
name: str,
type_: Type[Any],
class_validators: Optional[Dict[str, Validator]] = None,
default: Optional[Any] = None,
required: Union[bool, UndefinedType] = True,
default: Optional[Any] = Undefined,
required: Union[bool, UndefinedType] = Undefined,
model_config: Type[BaseConfig] = BaseConfig,
field_info: Optional[FieldInfo] = None,
alias: Optional[str] = None,
@ -86,20 +74,27 @@ def create_response_field(
Create a new response field. Raises if type_ is invalid.
"""
class_validators = class_validators or {}
field_info = field_info or FieldInfo()
try:
return ModelField(
name=name,
type_=type_,
class_validators=class_validators,
default=default,
required=required,
model_config=model_config,
alias=alias,
field_info=field_info,
if PYDANTIC_V2:
field_info = field_info or FieldInfo(
annotation=type_, default=default, alias=alias
)
else:
field_info = field_info or FieldInfo()
kwargs = {"name": name, "field_info": field_info}
if not PYDANTIC_V2:
kwargs.update(
{
"type_": type_,
"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(
"Invalid args for response field! Hint: "
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,
) -> ModelField:
if PYDANTIC_V2:
return field
# cloned_types caches already cloned types to support recursive models and improve
# performance by avoiding unecessary cloning
if cloned_types is None:
@ -136,30 +133,30 @@ def create_cloned_field(
f, cloned_types=cloned_types
)
new_field = create_response_field(name=field.name, type_=use_type)
new_field.has_alias = field.has_alias
new_field.alias = field.alias
new_field.class_validators = field.class_validators
new_field.default = field.default
new_field.required = field.required
new_field.model_config = field.model_config
new_field.has_alias = field.has_alias # type: ignore[attr-defined]
new_field.alias = field.alias # type: ignore[misc]
new_field.class_validators = field.class_validators # type: ignore[attr-defined]
new_field.default = field.default # type: ignore[misc]
new_field.required = field.required # type: ignore[misc]
new_field.model_config = field.model_config # type: ignore[attr-defined]
new_field.field_info = field.field_info
new_field.allow_none = field.allow_none
new_field.validate_always = field.validate_always
if field.sub_fields:
new_field.sub_fields = [
new_field.allow_none = field.allow_none # type: ignore[attr-defined]
new_field.validate_always = field.validate_always # type: ignore[attr-defined]
if field.sub_fields: # type: ignore[attr-defined]
new_field.sub_fields = [ # type: ignore[attr-defined]
create_cloned_field(sub_field, cloned_types=cloned_types)
for sub_field in field.sub_fields
for sub_field in field.sub_fields # type: ignore[attr-defined]
]
if field.key_field:
new_field.key_field = create_cloned_field(
field.key_field, cloned_types=cloned_types
if field.key_field: # type: ignore[attr-defined]
new_field.key_field = create_cloned_field( # type: ignore[attr-defined]
field.key_field, cloned_types=cloned_types # type: ignore[attr-defined]
)
new_field.validators = field.validators
new_field.pre_validators = field.pre_validators
new_field.post_validators = field.post_validators
new_field.parse_json = field.parse_json
new_field.shape = field.shape
new_field.populate_validators()
new_field.validators = field.validators # type: ignore[attr-defined]
new_field.pre_validators = field.pre_validators # type: ignore[attr-defined]
new_field.post_validators = field.post_validators # type: ignore[attr-defined]
new_field.parse_json = field.parse_json # type: ignore[attr-defined]
new_field.shape = field.shape # type: ignore[attr-defined]
new_field.populate_validators() # type: ignore[attr-defined]
return new_field
@ -220,3 +217,7 @@ def get_value_or_default(
if not isinstance(item, DefaultPlaceholder):
return 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 = [
"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"]
@ -59,7 +59,7 @@ all = [
"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",
"orjson >=3.2.1",
"email_validator >=1.1.1",
"email_validator >=2.0.0",
"uvicorn[standard] >=0.12.0",
]
@ -83,6 +83,7 @@ check_untyped_defs = true
addopts = [
"--strict-config",
"--strict-markers",
"--ignore=docs_src",
]
xfail_strict = true
junit_family = "xunit2"

4
requirements-tests.txt

@ -1,11 +1,13 @@
-e .
pytest >=7.1.3,<8.0.0
coverage[toml] >= 6.5.0,< 8.0
dirty-equals >= 0.6.0
mypy ==1.3.0
ruff ==0.0.272
black == 23.3.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
# probably when including SQLModel
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.testclient import TestClient
from pydantic import BaseModel, HttpUrl
@ -42,13 +43,24 @@ def test_openapi_schema():
"parameters": [
{
"required": True,
"schema": {
"title": "Callback Url",
"maxLength": 2083,
"minLength": 1,
"type": "string",
"format": "uri",
},
"schema": IsDict(
{
"title": "Callback Url",
"minLength": 1,
"type": "string",
"format": "uri",
}
)
# TODO: remove when deprecating Pydantic v1
| IsDict(
{
"title": "Callback Url",
"maxLength": 2083,
"minLength": 1,
"type": "string",
"format": "uri",
}
),
"name": "callback_url",
"in": "query",
}

49
tests/test_annotated.py

@ -1,6 +1,8 @@
import pytest
from dirty_equals import IsDict
from fastapi import APIRouter, FastAPI, Query
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from typing_extensions import Annotated
app = FastAPI()
@ -30,21 +32,46 @@ client = TestClient(app)
foo_is_missing = {
"detail": [
{
"loc": ["query", "foo"],
"msg": "field required",
"type": "value_error.missing",
}
IsDict(
{
"loc": ["query", "foo"],
"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 = {
"detail": [
{
"ctx": {"limit_value": 1},
"loc": ["query", "foo"],
"msg": "ensure this value has at least 1 characters",
"type": "value_error.any_str.min_length",
}
IsDict(
{
"ctx": {"min_length": 1},
"loc": ["query", "foo"],
"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
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from .main import app
@ -266,10 +267,17 @@ def test_openapi_schema():
"operationId": "get_path_param_id_path_param__item_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"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",
"parameters": [
{
"required": False,
"schema": {"title": "Query", "type": "integer"},
"name": "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._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel
@ -8,10 +9,18 @@ app = FastAPI()
class Item(BaseModel):
name: str
class Config:
schema_extra = {
"x-something-internal": {"level": 4},
if PYDANTIC_V2:
model_config = {
"json_schema_extra": {
"x-something-internal": {"level": 4},
}
}
else:
class Config:
schema_extra = {
"x-something-internal": {"level": 4},
}
@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 pydantic import BaseModel
from .utils import needs_pydanticv1, needs_pydanticv2
class ModelWithDatetimeField(BaseModel):
dt_field: datetime
class Config:
json_encoders = {
datetime: lambda dt: dt.replace(
microsecond=0, tzinfo=timezone.utc
).isoformat()
}
@needs_pydanticv2
def test_pydanticv2():
from pydantic import field_serializer
class ModelWithDatetimeField(BaseModel):
dt_field: datetime
app = FastAPI()
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
@field_serializer("dt_field")
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)
def get_model():
return model
@app.get("/model", response_model=ModelWithDatetimeField)
def get_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:
response = client.get("/model")
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 dirty_equals import IsDict
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from pydantic import BaseModel
app = FastAPI()
@ -47,15 +49,30 @@ async def no_duplicates_sub(
def test_no_duplicates_invalid():
response = client.post("/no-duplicates", json={"item": {"data": "myitem"}})
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"loc": ["body", "item2"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
assert response.json() == IsDict(
{
"detail": [
{
"type": "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():

666
tests/test_dependency_overrides.py

@ -1,8 +1,10 @@
from typing import Optional
import pytest
from dirty_equals import IsDict
from fastapi import APIRouter, Depends, FastAPI
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
app = FastAPI()
@ -50,99 +52,180 @@ async def overrider_dependency_with_sub(msg: dict = Depends(overrider_sub_depend
return msg
@pytest.mark.parametrize(
"url,status_code,expected",
[
(
"/main-depends/",
422,
{
"detail": [
{
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
(
"/main-depends/?q=foo",
200,
{"in": "main-depends", "params": {"q": "foo", "skip": 0, "limit": 100}},
),
(
"/main-depends/?q=foo&skip=100&limit=200",
200,
{"in": "main-depends", "params": {"q": "foo", "skip": 100, "limit": 200}},
),
(
"/decorator-depends/",
422,
{
"detail": [
{
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
("/decorator-depends/?q=foo", 200, {"in": "decorator-depends"}),
(
"/decorator-depends/?q=foo&skip=100&limit=200",
200,
{"in": "decorator-depends"},
),
(
"/router-depends/",
422,
{
"detail": [
{
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
(
"/router-depends/?q=foo",
200,
{"in": "router-depends", "params": {"q": "foo", "skip": 0, "limit": 100}},
),
(
"/router-depends/?q=foo&skip=100&limit=200",
200,
{"in": "router-depends", "params": {"q": "foo", "skip": 100, "limit": 200}},
),
(
"/router-decorator-depends/",
422,
{
"detail": [
{
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
("/router-decorator-depends/?q=foo", 200, {"in": "router-decorator-depends"}),
(
"/router-decorator-depends/?q=foo&skip=100&limit=200",
200,
{"in": "router-decorator-depends"},
),
],
)
def test_normal_app(url, status_code, expected):
response = client.get(url)
assert response.status_code == status_code
assert response.json() == expected
def test_main_depends():
response = client.get("/main-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_main_depends_q_foo():
response = client.get("/main-depends/?q=foo")
assert response.status_code == 200
assert response.json() == {
"in": "main-depends",
"params": {"q": "foo", "skip": 0, "limit": 100},
}
def test_main_depends_q_foo_skip_100_limit_200():
response = client.get("/main-depends/?q=foo&skip=100&limit=200")
assert response.status_code == 200
assert response.json() == {
"in": "main-depends",
"params": {"q": "foo", "skip": 100, "limit": 200},
}
def test_decorator_depends():
response = client.get("/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_decorator_depends_q_foo():
response = client.get("/decorator-depends/?q=foo")
assert response.status_code == 200
assert response.json() == {"in": "decorator-depends"}
def test_decorator_depends_q_foo_skip_100_limit_200():
response = client.get("/decorator-depends/?q=foo&skip=100&limit=200")
assert response.status_code == 200
assert response.json() == {"in": "decorator-depends"}
def test_router_depends():
response = client.get("/router-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_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(
@ -190,126 +273,281 @@ def test_override_simple(url, status_code, expected):
app.dependency_overrides = {}
@pytest.mark.parametrize(
"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):
def test_override_with_sub_main_depends():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get(url)
assert response.status_code == status_code
assert response.json() == expected
response = client.get("/main-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__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 = {}

10
tests/test_extra_routes.py

@ -1,5 +1,6 @@
from typing import Optional
from dirty_equals import IsDict
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
@ -327,7 +328,14 @@ def test_openapi_schema():
"type": "object",
"properties": {
"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": {

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
from fastapi import Depends, FastAPI
from fastapi.exceptions import ResponseValidationError
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")
assert response.status_code == 200, response.text
assert response.json() == {
@ -50,8 +24,9 @@ def test_filter_sub_model():
}
def test_validator_is_cloned():
with pytest.raises(ValidationError) as err:
@needs_pydanticv1
def test_validator_is_cloned(client: TestClient):
with pytest.raises(ResponseValidationError) as err:
client.get("/model/modelX")
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")
assert response.status_code == 200, response.text
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 dirty_equals import IsDict
from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient
@ -104,35 +105,253 @@ def test_get_users_item():
assert response.json() == {"item_id": "item01", "user_id": "abc123"}
def test_schema_1():
"""Check that the user_id is a required path parameter under /users"""
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
r = response.json()
d = {
"required": True,
"schema": {"title": "User Id", "type": "string"},
"name": "user_id",
"in": "path",
assert response.json() == {
"openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/users/": {
"get": {
"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 pydantic import BaseModel
app = FastAPI()
from .utils import needs_pydanticv1, needs_pydanticv2
class MyUuid:
@ -26,40 +26,78 @@ class MyUuid:
raise TypeError("vars() argument must have __dict__ attribute")
@app.get("/fast_uuid")
def return_fast_uuid():
# I don't want to import asyncpg for this test so I made my own UUID
# Import asyncpg and uncomment the two lines below for the actual bug
@needs_pydanticv2
def test_pydanticv2():
from pydantic import field_serializer
# from asyncpg.pgproto import pgproto
# asyncpg_uuid = pgproto.UUID("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
app = FastAPI()
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}
@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):
model_config = {"arbitrary_types_allowed": True}
class SomeCustomClass(BaseModel):
class Config:
arbitrary_types_allowed = True
json_encoders = {uuid.UUID: str}
a_uuid: MyUuid
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")
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)
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:
response_simple = client.get("/fast_uuid")
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
import pytest
from fastapi._compat import PYDANTIC_V2
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:
@ -45,22 +48,6 @@ class Unserializable:
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):
admin = "admin"
normal = "normal"
@ -69,8 +56,12 @@ class RoleEnum(Enum):
class ModelWithConfig(BaseModel):
role: Optional[RoleEnum] = None
class Config:
use_enum_values = True
if PYDANTIC_V2:
model_config = {"use_enum_values": True}
else:
class Config:
use_enum_values = True
class ModelWithAlias(BaseModel):
@ -83,23 +74,6 @@ class ModelWithDefault(BaseModel):
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():
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)
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))
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():
model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8))
# TODO: remove when deprecating Pydantic v1
@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"}
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():
@ -196,6 +203,7 @@ def test_encode_model_with_default():
}
@needs_pydanticv1
def test_custom_encoders():
class safe_datetime(datetime):
pass
@ -226,14 +234,55 @@ def test_custom_enum_encoders():
assert encoded_instance == custom_enum_encoder(instance)
def test_encode_model_with_path(model_with_path):
if isinstance(model_with_path.path, PureWindowsPath):
expected = "\\foo\\bar"
else:
expected = "/foo/bar"
assert jsonable_encoder(model_with_path) == {"path": expected}
def test_encode_model_with_pure_path():
class ModelWithPath(BaseModel):
path: PurePath
if PYDANTIC_V2:
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():
class ModelWithRoot(BaseModel):
__root__: str
model = ModelWithRoot(__root__="Foo")
assert jsonable_encoder(model) == "Foo"

160
tests/test_multi_body_errors.py

@ -1,8 +1,10 @@
from decimal import Decimal
from typing import List
from dirty_equals import IsDict, IsOneOf
from fastapi import FastAPI
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from pydantic import BaseModel, condecimal
app = FastAPI()
@ -21,59 +23,115 @@ def save_item_no_body(item: List[Item]):
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():
response = client.post("/items/", json=[{"name": "Foo", "age": 5}])
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():
response = client.post("/items/", json=[{"name": "Foo", "age": -1.0}])
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():
response = client.post("/items/", json=[{"age": "five"}, {"age": "six"}])
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():
@ -126,11 +184,23 @@ def test_openapi_schema():
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"age": {
"title": "Age",
"exclusiveMinimum": 0.0,
"type": "number",
},
"age": IsDict(
{
"title": "Age",
"anyOf": [
{"exclusiveMinimum": 0.0, "type": "number"},
{"type": "string"},
],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "Age",
"exclusiveMinimum": 0.0,
"type": "number",
}
),
},
},
"ValidationError": {

55
tests/test_multi_query_errors.py

@ -1,7 +1,9 @@
from typing import List
from dirty_equals import IsDict
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
app = FastAPI()
@ -14,22 +16,6 @@ def read_items(q: List[int] = Query(default=None)):
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():
response = client.get("/items/?q=5&q=6")
assert response.status_code == 200, response.text
@ -39,7 +25,42 @@ def test_multi_query():
def test_multi_query_incorrect():
response = client.get("/items/?q=five&q=six")
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():

21
tests/test_openapi_query_parameter_extension.py

@ -1,5 +1,6 @@
from typing import Optional
from dirty_equals import IsDict
from fastapi import FastAPI
from fastapi.testclient import TestClient
@ -52,11 +53,21 @@ def test_openapi():
"parameters": [
{
"required": False,
"schema": {
"title": "Standard Query Param",
"type": "integer",
"default": 50,
},
"schema": IsDict(
{
"anyOf": [{"type": "integer"}, {"type": "null"}],
"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",
"in": "query",
},

15
tests/test_openapi_servers.py

@ -1,3 +1,4 @@
from dirty_equals import IsOneOf
from fastapi import FastAPI
from fastapi.testclient import TestClient
@ -35,10 +36,20 @@ def test_openapi_schema():
"servers": [
{"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",
},
{"url": "https://prod.example.com"},
{
"url": IsOneOf(
"https://prod.example.com/",
# TODO: remove when deprecating Pydantic v1
"https://prod.example.com",
)
},
],
"paths": {
"/foo": {

135
tests/test_params_repr.py

@ -1,6 +1,6 @@
from typing import Any, List
import pytest
from dirty_equals import IsOneOf
from fastapi.params import Body, Cookie, Depends, Header, Param, Path, Query
test_data: List[Any] = ["teststr", None, ..., 1, []]
@ -10,34 +10,137 @@ def get_user():
return {} # pragma: no cover
@pytest.fixture(scope="function", params=test_data)
def params(request):
return request.param
def test_param_repr_str():
assert repr(Param("teststr")) == "Param(teststr)"
def test_param_repr(params):
assert repr(Param(params)) == "Param(" + str(params) + ")"
def test_param_repr_none():
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():
assert repr(Path()) == "Path(Ellipsis)"
assert repr(Path(...)) == "Path(Ellipsis)"
assert repr(Path()) == IsOneOf(
"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):
assert repr(Query(params)) == "Query(" + str(params) + ")"
def test_query_repr_str():
assert repr(Query("teststr")) == "Query(teststr)"
def test_header_repr(params):
assert repr(Header(params)) == "Header(" + str(params) + ")"
def test_query_repr_none():
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):
assert repr(Cookie(params)) == "Cookie(" + str(params) + ")"
def test_body_repr_number():
assert repr(Body(1)) == "Body(1)"
def test_body_repr(params):
assert repr(Body(params)) == "Body(" + str(params) + ")"
def test_body_repr_list():
assert repr(Body([])) == "Body([])"
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.utils import match_pydantic_error_url
from .main import app
client = TestClient(app)
response_missing = {
"detail": [
{
"loc": ["query", "query"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
response_not_valid_int = {
"detail": [
{
"loc": ["query", "query"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
@pytest.mark.parametrize(
"path,expected_status,expected_response",
[
("/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),
("/query/int?query=42", 200, "foo bar 42"),
("/query/int?query=42.5", 422, response_not_valid_int),
("/query/int?query=baz", 422, response_not_valid_int),
("/query/int?not_declared=baz", 422, response_missing),
("/query/int/optional", 200, "foo bar"),
("/query/int/optional?query=50", 200, "foo bar 50"),
("/query/int/optional?query=foo", 422, response_not_valid_int),
("/query/int/default", 200, "foo bar 10"),
("/query/int/default?query=50", 200, "foo bar 50"),
("/query/int/default?query=foo", 422, response_not_valid_int),
("/query/param", 200, "foo bar"),
("/query/param?query=50", 200, "foo bar 50"),
("/query/param-required", 422, response_missing),
("/query/param-required?query=50", 200, "foo bar 50"),
("/query/param-required/int", 422, response_missing),
("/query/param-required/int?query=50", 200, "foo bar 50"),
("/query/param-required/int?query=foo", 422, response_not_valid_int),
("/query/frozenset/?query=1&query=1&query=2", 200, "1,2"),
],
)
def test_get_path(path, expected_status, expected_response):
response = client.get(path)
assert response.status_code == expected_status
assert response.json() == expected_response
def test_query():
response = client.get("/query")
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_query_baz():
response = client.get("/query?query=baz")
assert response.status_code == 200
assert response.json() == "foo bar baz"
def test_query_not_declared_baz():
response = client.get("/query?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_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.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):
@property
def full_name(self) -> str:
return f"{self.name} {self.lastname}"
class PersonRead(PersonBase):
full_name: str
class Config:
orm_mode = True
read_with_orm_mode = True
model_config = {"from_attributes": True}
app = FastAPI()
class PersonCreate(PersonBase):
pass
@app.post("/people/", response_model=PersonRead)
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):
full_name: str
@needs_pydanticv1
def test_read_with_orm_mode_pv1() -> None:
class PersonBase(BaseModel):
name: str
lastname: str
class Config:
orm_mode = True
class Person(PersonBase):
@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)
def create_person(person: PersonCreate) -> Any:
db_person = Person.from_orm(person)
return db_person
class Config:
orm_mode = True
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"}
response = client.post("/people/", json=person_data)
data = response.json()

1
tests/test_request_body_parameters_media_type.py

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

28
tests/test_response_by_alias.py

@ -1,8 +1,9 @@
from typing import List
from fastapi import FastAPI
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from pydantic import BaseModel, ConfigDict, Field
app = FastAPI()
@ -14,13 +15,24 @@ class Model(BaseModel):
class ModelNoAlias(BaseModel):
name: str
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"
)
}
if PYDANTIC_V2:
model_config = ConfigDict(
json_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"
)
}
)
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)

16
tests/test_response_model_as_return_annotation.py

@ -2,10 +2,10 @@ from typing import List, Union
import pytest
from fastapi import FastAPI
from fastapi.exceptions import FastAPIError
from fastapi.exceptions import FastAPIError, ResponseValidationError
from fastapi.responses import JSONResponse, Response
from fastapi.testclient import TestClient
from pydantic import BaseModel, ValidationError
from pydantic import 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():
with pytest.raises(ValidationError):
with pytest.raises(ResponseValidationError):
client.get("/response_model-no_annotation-return_invalid_dict")
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")
@ -313,12 +313,12 @@ def test_no_response_model_annotation_return_exact_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")
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")
@ -395,12 +395,12 @@ def test_response_model_model1_annotation_model2_return_exact_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")
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")

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 dirty_equals import IsDict
from fastapi import Body, Cookie, FastAPI, Header, Path, Query
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
app = FastAPI()
@ -10,8 +12,14 @@ app = FastAPI()
class Item(BaseModel):
data: str
class Config:
schema_extra = {"example": {"data": "Data in schema_extra"}}
if PYDANTIC_V2:
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/")
@ -537,7 +545,16 @@ def test_openapi_schema():
"parameters": [
{
"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",
"name": "data",
"in": "query",
@ -568,7 +585,16 @@ def test_openapi_schema():
"parameters": [
{
"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": {
"example1": {
"summary": "Query example 1",
@ -605,7 +631,16 @@ def test_openapi_schema():
"parameters": [
{
"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": {
"example1": {
"summary": "Query example 1",
@ -642,7 +677,16 @@ def test_openapi_schema():
"parameters": [
{
"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",
"name": "data",
"in": "header",
@ -673,7 +717,16 @@ def test_openapi_schema():
"parameters": [
{
"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": {
"example1": {
"summary": "header example 1",
@ -710,7 +763,16 @@ def test_openapi_schema():
"parameters": [
{
"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": {
"example1": {
"summary": "Query example 1",
@ -747,7 +809,16 @@ def test_openapi_schema():
"parameters": [
{
"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",
"name": "data",
"in": "cookie",
@ -778,7 +849,16 @@ def test_openapi_schema():
"parameters": [
{
"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": {
"example1": {
"summary": "cookie example 1",
@ -815,7 +895,16 @@ def test_openapi_schema():
"parameters": [
{
"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": {
"example1": {
"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.security import OAuth2, OAuth2PasswordRequestFormStrict
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from pydantic import BaseModel
app = FastAPI()
@ -59,76 +60,136 @@ def test_security_oauth2_password_bearer_no_header():
assert response.json() == {"detail": "Not authenticated"}
required_params = {
"detail": [
def test_strict_login_no_data():
response = client.post("/login")
assert response.status_code == 422
assert response.json() == IsDict(
{
"loc": ["body", "grant_type"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "username"],
"msg": "field required",
"type": "value_error.missing",
},
"detail": [
{
"type": "missing",
"loc": ["body", "grant_type"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("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"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
"detail": [
{
"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"],
"msg": "field required",
"type": "value_error.missing",
"detail": [
{
"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"],
"msg": 'string does not match regex "password"',
"type": "value_error.str.regex",
"ctx": {"pattern": "password"},
"detail": [
{
"type": "string_pattern_mismatch",
"loc": ["body", "grant_type"],
"msg": "String should match pattern 'password'",
"input": "incorrect",
"ctx": {"pattern": "password"},
"url": match_pydantic_error_url("string_pattern_mismatch"),
}
]
}
]
}
@pytest.mark.parametrize(
"data,expected_status,expected_response",
[
(None, 422, required_params),
({"username": "johndoe", "password": "secret"}, 422, grant_type_required),
(
{"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
422,
grant_type_incorrect,
),
(
{"username": "johndoe", "password": "secret", "grant_type": "password"},
200,
{
"grant_type": "password",
"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
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "grant_type"],
"msg": 'string does not match regex "password"',
"type": "value_error.str.regex",
"ctx": {"pattern": "password"},
}
]
}
)
def test_strict_login_correct_grant_type():
response = client.post(
"/login",
data={"username": "johndoe", "password": "secret", "grant_type": "password"},
)
assert response.status_code == 200
assert response.json() == {
"grant_type": "password",
"username": "johndoe",
"password": "secret",
"scopes": [],
"client_id": None,
"client_secret": None,
}
def test_openapi_schema():
@ -199,8 +260,26 @@ def test_openapi_schema():
"username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""},
"client_id": {"title": "Client Id", "type": "string"},
"client_secret": {"title": "Client Secret", "type": "string"},
"client_id": IsDict(
{
"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": {

209
tests/test_security_oauth2_optional.py

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

209
tests/test_security_oauth2_optional_description.py

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

2
tests/test_skip_defaults.py

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

43
tests/test_sub_callbacks.py

@ -1,5 +1,6 @@
from typing import Optional
from dirty_equals import IsDict
from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, HttpUrl
@ -98,13 +99,30 @@ def test_openapi_schema():
"parameters": [
{
"required": False,
"schema": {
"title": "Callback Url",
"maxLength": 2083,
"minLength": 1,
"type": "string",
"format": "uri",
},
"schema": IsDict(
{
"title": "Callback Url",
"anyOf": [
{
"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",
"in": "query",
}
@ -244,7 +262,16 @@ def test_openapi_schema():
"type": "object",
"properties": {
"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"},
"total": {"title": "Total", "type": "number"},
},

91
tests/test_tuples.py

@ -1,5 +1,6 @@
from typing import List, Tuple
from dirty_equals import IsDict
from fastapi import FastAPI, Form
from fastapi.testclient import TestClient
from pydantic import BaseModel
@ -126,16 +127,31 @@ def test_openapi_schema():
"requestBody": {
"content": {
"application/json": {
"schema": {
"title": "Square",
"maxItems": 2,
"minItems": 2,
"type": "array",
"items": [
{"$ref": "#/components/schemas/Coordinate"},
{"$ref": "#/components/schemas/Coordinate"},
],
}
"schema": IsDict(
{
"title": "Square",
"maxItems": 2,
"minItems": 2,
"type": "array",
"prefixItems": [
{"$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,
@ -198,13 +214,28 @@ def test_openapi_schema():
"required": ["values"],
"type": "object",
"properties": {
"values": {
"title": "Values",
"maxItems": 2,
"minItems": 2,
"type": "array",
"items": [{"type": "integer"}, {"type": "integer"}],
}
"values": IsDict(
{
"title": "Values",
"maxItems": 2,
"minItems": 2,
"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": {
@ -235,12 +266,26 @@ def test_openapi_schema():
"items": {
"title": "Items",
"type": "array",
"items": {
"maxItems": 2,
"minItems": 2,
"type": "array",
"items": [{"type": "string"}, {"type": "string"}],
},
"items": IsDict(
{
"maxItems": 2,
"minItems": 2,
"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 shutil
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.additional_responses.tutorial002 import app
@ -64,7 +65,16 @@ def test_openapi_schema():
},
{
"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",
"in": "query",
},

12
tests/test_tutorial/test_additional_responses/test_tutorial004.py

@ -1,6 +1,7 @@
import os
import shutil
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.additional_responses.tutorial004 import app
@ -67,7 +68,16 @@ def test_openapi_schema():
},
{
"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",
"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 ...utils import needs_pydanticv1
# TODO: pv2 add version with Pydantic v2
@needs_pydanticv1
def test_create_read():
with TestClient(app) as client:
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 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"}
def test_openapi():
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == {
@ -19,9 +20,20 @@ def test_openapi():
"info": {"title": "FastAPI", "version": "0.1.0"},
"servers": [
{"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",
},
],

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 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"}
def test_openapi():
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == {
"openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"},
"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",
},
],

476
tests/test_tutorial/test_bigger_applications/test_main.py

@ -1,138 +1,368 @@
import pytest
from dirty_equals import IsDict
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"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
@pytest.mark.parametrize(
"path,expected_status,expected_response,headers",
[
(
"/users?token=jessica",
200,
[{"username": "Rick"}, {"username": "Morty"}],
{},
),
("/users", 422, no_jessica, {}),
("/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,
{"detail": "No Jessica token provided"},
{},
),
(
"/items?token=jessica",
200,
{"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}},
{"X-Token": "fake-super-secret-token"},
),
("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}),
(
"/items/plumbus?token=jessica",
200,
{"name": "Plumbus", "item_id": "plumbus"},
{"X-Token": "fake-super-secret-token"},
),
(
"/items/bar?token=jessica",
404,
{"detail": "Item not found"},
{"X-Token": "fake-super-secret-token"},
),
("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}),
(
"/items?token=jessica",
400,
{"detail": "X-Token header invalid"},
{"X-Token": "invalid"},
),
(
"/items/bar?token=jessica",
400,
{"detail": "X-Token header invalid"},
{"X-Token": "invalid"},
),
(
"/items?token=jessica",
422,
{
"detail": [
{
"loc": ["header", "x-token"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
{},
),
(
"/items/plumbus?token=jessica",
422,
{
"detail": [
{
"loc": ["header", "x-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
assert response.json() == expected_response
def test_put_no_header():
response = client.put("/items/foo")
assert response.status_code == 422, response.text
"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_users_foo_token_jessica(client: TestClient):
response = client.get("/users/foo?token=jessica")
assert response.status_code == 200
assert response.json() == {"username": "foo"}
def test_users_foo_with_no_token(client: TestClient):
response = client.get("/users/foo")
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_users_me_token_jessica(client: TestClient):
response = client.get("/users/me?token=jessica")
assert response.status_code == 200
assert response.json() == {"username": "fakecurrentuser"}
def test_users_me_with_no_token(client: TestClient):
response = client.get("/users/me")
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_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"}
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() == {
"detail": [
{
"loc": ["query", "token"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["header", "x-token"],
"msg": "field required",
"type": "value_error.missing",
},
]
"plumbus": {"name": "Plumbus"},
"gun": {"name": "Portal Gun"},
}
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"})
assert response.status_code == 400, response.text
assert response.json() == {"detail": "X-Token header invalid"}
def test_put():
def test_put(client: TestClient):
response = client.put(
"/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"}
def test_put_forbidden():
def test_put_forbidden(client: TestClient):
response = client.put(
"/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"}
def test_admin():
def test_admin(client: TestClient):
response = client.post(
"/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"}
)
@ -156,13 +386,13 @@ def test_admin():
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"})
assert response.status_code == 400, response.text
assert response.json() == {"detail": "X-Token header invalid"}
def test_openapi_schema():
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {

476
tests/test_tutorial/test_bigger_applications/test_main_an.py

@ -1,138 +1,368 @@
import pytest
from dirty_equals import IsDict
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"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
@pytest.mark.parametrize(
"path,expected_status,expected_response,headers",
[
(
"/users?token=jessica",
200,
[{"username": "Rick"}, {"username": "Morty"}],
{},
),
("/users", 422, no_jessica, {}),
("/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,
{"detail": "No Jessica token provided"},
{},
),
(
"/items?token=jessica",
200,
{"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}},
{"X-Token": "fake-super-secret-token"},
),
("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}),
(
"/items/plumbus?token=jessica",
200,
{"name": "Plumbus", "item_id": "plumbus"},
{"X-Token": "fake-super-secret-token"},
),
(
"/items/bar?token=jessica",
404,
{"detail": "Item not found"},
{"X-Token": "fake-super-secret-token"},
),
("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}),
(
"/items?token=jessica",
400,
{"detail": "X-Token header invalid"},
{"X-Token": "invalid"},
),
(
"/items/bar?token=jessica",
400,
{"detail": "X-Token header invalid"},
{"X-Token": "invalid"},
),
(
"/items?token=jessica",
422,
{
"detail": [
{
"loc": ["header", "x-token"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
{},
),
(
"/items/plumbus?token=jessica",
422,
{
"detail": [
{
"loc": ["header", "x-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
assert response.json() == expected_response
def test_put_no_header():
response = client.put("/items/foo")
assert response.status_code == 422, response.text
"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_users_foo_token_jessica(client: TestClient):
response = client.get("/users/foo?token=jessica")
assert response.status_code == 200
assert response.json() == {"username": "foo"}
def test_users_foo_with_no_token(client: TestClient):
response = client.get("/users/foo")
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_users_me_token_jessica(client: TestClient):
response = client.get("/users/me?token=jessica")
assert response.status_code == 200
assert response.json() == {"username": "fakecurrentuser"}
def test_users_me_with_no_token(client: TestClient):
response = client.get("/users/me")
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_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"}
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() == {
"detail": [
{
"loc": ["query", "token"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["header", "x-token"],
"msg": "field required",
"type": "value_error.missing",
},
]
"plumbus": {"name": "Plumbus"},
"gun": {"name": "Portal Gun"},
}
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"})
assert response.status_code == 400, response.text
assert response.json() == {"detail": "X-Token header invalid"}
def test_put():
def test_put(client: TestClient):
response = client.put(
"/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"}
def test_put_forbidden():
def test_put_forbidden(client: TestClient):
response = client.put(
"/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"}
def test_admin():
def test_admin(client: TestClient):
response = client.post(
"/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"}
)
@ -156,13 +386,13 @@ def test_admin():
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"})
assert response.status_code == 400, response.text
assert response.json() == {"detail": "X-Token header invalid"}
def test_openapi_schema():
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {

470
tests/test_tutorial/test_bigger_applications/test_main_an_py39.py

@ -1,18 +1,10 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py39
no_jessica = {
"detail": [
{
"loc": ["query", "token"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
@pytest.fixture(name="client")
def get_client():
@ -23,116 +15,366 @@ def get_client():
@needs_py39
@pytest.mark.parametrize(
"path,expected_status,expected_response,headers",
[
(
"/users?token=jessica",
200,
[{"username": "Rick"}, {"username": "Morty"}],
{},
),
("/users", 422, no_jessica, {}),
("/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,
{"detail": "No Jessica token provided"},
{},
),
(
"/items?token=jessica",
200,
{"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}},
{"X-Token": "fake-super-secret-token"},
),
("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}),
(
"/items/plumbus?token=jessica",
200,
{"name": "Plumbus", "item_id": "plumbus"},
{"X-Token": "fake-super-secret-token"},
),
(
"/items/bar?token=jessica",
404,
{"detail": "Item not found"},
{"X-Token": "fake-super-secret-token"},
),
("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}),
(
"/items?token=jessica",
400,
{"detail": "X-Token header invalid"},
{"X-Token": "invalid"},
),
(
"/items/bar?token=jessica",
400,
{"detail": "X-Token header invalid"},
{"X-Token": "invalid"},
),
(
"/items?token=jessica",
422,
{
"detail": [
{
"loc": ["header", "x-token"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
{},
),
(
"/items/plumbus?token=jessica",
422,
{
"detail": [
{
"loc": ["header", "x-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, client: TestClient
):
response = client.get(path, headers=headers)
assert response.status_code == expected_status
assert response.json() == expected_response
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"}]
@needs_py39
def test_users_with_no_token(client: TestClient):
response = client.get("/users")
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_users_foo_token_jessica(client: TestClient):
response = client.get("/users/foo?token=jessica")
assert response.status_code == 200
assert response.json() == {"username": "foo"}
@needs_py39
def test_users_foo_with_no_token(client: TestClient):
response = client.get("/users/foo")
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_users_me_token_jessica(client: TestClient):
response = client.get("/users/me?token=jessica")
assert response.status_code == 200
assert response.json() == {"username": "fakecurrentuser"}
@needs_py39
def test_users_me_with_no_token(client: TestClient):
response = client.get("/users/me")
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_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
def test_put_no_header(client: TestClient):
response = client.put("/items/foo")
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"loc": ["query", "token"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["header", "x-token"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
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",
},
]
}
)
@needs_py39

459
tests/test_tutorial/test_body/test_tutorial001.py

@ -1,134 +1,268 @@
from unittest.mock import patch
import pytest
from dirty_equals import IsDict
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"],
"msg": "field required",
"type": "value_error.missing",
"detail": [
{
"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 = {
"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", "price"],
"msg": "value is not a valid float",
"type": "type_error.float",
"detail": [
{
"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"],
"msg": "field required",
"type": "value_error.missing",
},
"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
{
"loc": ["body", "price"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
body_missing = {
"detail": [
{"loc": ["body"], "msg": "field required", "type": "value_error.missing"}
]
}
@pytest.mark.parametrize(
"path,body,expected_status,expected_response",
[
(
"/items/",
{"name": "Foo", "price": 50.5},
200,
{"name": "Foo", "price": 50.5, "description": None, "tax": None},
),
(
"/items/",
{"name": "Foo", "price": "50.5"},
200,
{"name": "Foo", "price": 50.5, "description": None, "tax": None},
),
(
"/items/",
{"name": "Foo", "price": "50.5", "description": "Some Foo"},
200,
{"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": None},
),
(
"/items/",
{"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):
response = client.post(path, json=body)
assert response.status_code == expected_status
assert response.json() == expected_response
def test_post_broken_body():
"detail": [
{
"loc": ["body", "name"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "price"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
)
def test_post_with_none(client: TestClient):
response = client.post("/items/", json=None)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_post_broken_body(client: TestClient):
response = client.post(
"/items/",
headers={"content-type": "application/json"},
content="{some broken json}",
)
assert response.status_code == 422, response.text
assert response.json() == {
"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,
},
}
]
}
assert response.json() == IsDict(
{
"detail": [
{
"type": "json_invalid",
"loc": ["body", 1],
"msg": "JSON decode error",
"input": {},
"ctx": {
"error": "Expecting property name enclosed in double quotes"
},
}
]
}
) | 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})
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"loc": ["body"],
"msg": "value is not a valid dict",
"type": "type_error.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_explicit_content_type():
def test_explicit_content_type(client: TestClient):
response = client.post(
"/items/",
content='{"name": "Foo", "price": 50.5}',
@ -137,7 +271,7 @@ def test_explicit_content_type():
assert response.status_code == 200, response.text
def test_geo_json():
def test_geo_json(client: TestClient):
response = client.post(
"/items/",
content='{"name": "Foo", "price": 50.5}',
@ -146,7 +280,7 @@ def test_geo_json():
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(
"/items/",
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}'
invalid_dict = {
"detail": [
{
"loc": ["body"],
"msg": "value is not a valid dict",
"type": "type_error.dict",
}
]
}
response = client.post(
"/items/", content=data, headers={"Content-Type": "text/plain"}
)
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(
"/items/", content=data, headers={"Content-Type": "application/geo+json-seq"}
)
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(
"/items/", content=data, headers={"Content-Type": "application/not-really-json"}
)
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):
response = client.post("/items/", json={"test": "test2"})
assert response.status_code == 400, response.text
def test_openapi_schema():
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
@ -243,8 +438,26 @@ def test_openapi_schema():
"properties": {
"name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"},
"description": {"title": "Description", "type": "string"},
"tax": {"title": "Tax", "type": "number"},
"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"}
),
},
},
"ValidationError": {

432
tests/test_tutorial/test_body/test_tutorial001_py310.py

@ -1,7 +1,9 @@
from unittest.mock import patch
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py310
@ -14,86 +16,189 @@ def client():
return client
price_missing = {
"detail": [
@needs_py310
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"],
"msg": "field required",
"type": "value_error.missing",
"detail": [
{
"type": "missing",
"loc": ["body", "price"],
"msg": "Field required",
"input": {"name": "Foo"},
"url": match_pydantic_error_url("missing"),
}
]
}
]
}
price_not_float = {
"detail": [
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"loc": ["body", "price"],
"msg": "value is not a valid float",
"type": "type_error.float",
"detail": [
{
"loc": ["body", "price"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
]
}
)
name_price_missing = {
"detail": [
@needs_py310
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"],
"msg": "field required",
"type": "value_error.missing",
},
"detail": [
{
"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"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
"detail": [
{
"loc": ["body", "price"],
"msg": "value is not a valid float",
"type": "type_error.float",
}
]
}
)
body_missing = {
"detail": [
{"loc": ["body"], "msg": "field required", "type": "value_error.missing"}
]
}
@needs_py310
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
@pytest.mark.parametrize(
"path,body,expected_status,expected_response",
[
(
"/items/",
{"name": "Foo", "price": 50.5},
200,
{"name": "Foo", "price": 50.5, "description": None, "tax": None},
),
(
"/items/",
{"name": "Foo", "price": "50.5"},
200,
{"name": "Foo", "price": 50.5, "description": None, "tax": None},
),
(
"/items/",
{"name": "Foo", "price": "50.5", "description": "Some Foo"},
200,
{"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": None},
),
(
"/items/",
{"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
def test_post_with_none(client: TestClient):
response = client.post("/items/", json=None)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@needs_py310
@ -104,37 +209,69 @@ def test_post_broken_body(client: TestClient):
content="{some broken json}",
)
assert response.status_code == 422, response.text
assert response.json() == {
"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,
},
}
]
}
assert response.json() == IsDict(
{
"detail": [
{
"type": "json_invalid",
"loc": ["body", 1],
"msg": "JSON decode error",
"input": {},
"ctx": {
"error": "Expecting property name enclosed in double quotes"
},
}
]
}
) | 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
def test_post_form_for_json(client: TestClient):
response = client.post("/items/", data={"name": "Foo", "price": 50.5})
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"loc": ["body"],
"msg": "value is not a valid dict",
"type": "type_error.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
@ -175,32 +312,91 @@ def test_no_content_type_is_json(client: TestClient):
@needs_py310
def test_wrong_headers(client: TestClient):
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(
"/items/", content=data, headers={"Content-Type": "text/plain"}
)
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(
"/items/", content=data, headers={"Content-Type": "application/geo+json-seq"}
)
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(
"/items/", content=data, headers={"Content-Type": "application/not-really-json"}
)
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
@ -258,8 +454,26 @@ def test_openapi_schema(client: TestClient):
"properties": {
"name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"},
"description": {"title": "Description", "type": "string"},
"tax": {"title": "Tax", "type": "number"},
"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"}
),
},
},
"ValidationError": {

153
tests/test_tutorial/test_body_fields/test_tutorial001.py

@ -1,66 +1,82 @@
import pytest
from dirty_equals import IsDict
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": [
{
"ctx": {"limit_value": 0},
"loc": ["body", "item", "price"],
"msg": "ensure this value is greater than 0",
"type": "value_error.number.not_gt",
}
]
}
def test_items_5(client: TestClient):
response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}})
assert response.status_code == 200
assert response.json() == {
"item_id": 5,
"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(
"path,body,expected_status,expected_response",
[
(
"/items/5",
{"item": {"name": "Foo", "price": 3.0}},
200,
{
"item_id": 5,
"item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
},
),
(
"/items/6",
{
"item": {
"name": "Bar",
"price": 0.2,
"description": "Some bar",
"tax": "5.4",
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,
{
"item_id": 6,
"item": {
"name": "Bar",
"price": 0.2,
"description": "Some bar",
"tax": 5.4,
},
},
),
("/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
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"ctx": {"limit_value": 0},
"loc": ["body", "item", "price"],
"msg": "ensure this value is greater than 0",
"type": "value_error.number.not_gt",
}
]
}
)
def test_openapi_schema():
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
@ -116,18 +132,39 @@ def test_openapi_schema():
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"description": {
"title": "The description of the item",
"maxLength": 300,
"type": "string",
},
"description": IsDict(
{
"title": "The description of the item",
"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": {
"title": "Price",
"exclusiveMinimum": 0.0,
"type": "number",
"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": {

153
tests/test_tutorial/test_body_fields/test_tutorial001_an.py

@ -1,66 +1,82 @@
import pytest
from dirty_equals import IsDict
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": [
{
"ctx": {"limit_value": 0},
"loc": ["body", "item", "price"],
"msg": "ensure this value is greater than 0",
"type": "value_error.number.not_gt",
}
]
}
def test_items_5(client: TestClient):
response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}})
assert response.status_code == 200
assert response.json() == {
"item_id": 5,
"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(
"path,body,expected_status,expected_response",
[
(
"/items/5",
{"item": {"name": "Foo", "price": 3.0}},
200,
{
"item_id": 5,
"item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
},
),
(
"/items/6",
{
"item": {
"name": "Bar",
"price": 0.2,
"description": "Some bar",
"tax": "5.4",
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,
{
"item_id": 6,
"item": {
"name": "Bar",
"price": 0.2,
"description": "Some bar",
"tax": 5.4,
},
},
),
("/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
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"ctx": {"limit_value": 0},
"loc": ["body", "item", "price"],
"msg": "ensure this value is greater than 0",
"type": "value_error.number.not_gt",
}
]
}
)
def test_openapi_schema():
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
@ -116,18 +132,39 @@ def test_openapi_schema():
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"description": {
"title": "The description of the item",
"maxLength": 300,
"type": "string",
},
"description": IsDict(
{
"title": "The description of the item",
"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": {
"title": "Price",
"exclusiveMinimum": 0.0,
"type": "number",
"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": {

145
tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py

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

145
tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py

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

145
tests/test_tutorial/test_body_fields/test_tutorial001_py310.py

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

147
tests/test_tutorial/test_body_multiple_params/test_tutorial001.py

@ -1,52 +1,74 @@
import pytest
from dirty_equals import IsDict
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")
assert response.status_code == 200, response.text
assert response.json() == {
@ -87,7 +109,16 @@ def test_openapi_schema():
},
{
"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",
"in": "query",
},
@ -95,7 +126,19 @@ def test_openapi_schema():
"requestBody": {
"content": {
"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",
"properties": {
"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"},
"description": {"title": "Description", "type": "string"},
"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"}
),
},
},
"ValidationError": {

147
tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py

@ -1,52 +1,74 @@
import pytest
from dirty_equals import IsDict
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")
assert response.status_code == 200, response.text
assert response.json() == {
@ -87,7 +109,16 @@ def test_openapi_schema():
},
{
"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",
"in": "query",
},
@ -95,7 +126,19 @@ def test_openapi_schema():
"requestBody": {
"content": {
"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",
"properties": {
"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"},
"description": {"title": "Description", "type": "string"},
"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"}
),
},
},
"ValidationError": {

140
tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py

@ -1,5 +1,7 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py310
@ -12,45 +14,64 @@ def get_client():
return client
item_id_not_int = {
"detail": [
{
"loc": ["path", "item_id"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
@needs_py310
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",
}
@needs_py310
@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, client: TestClient):
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"}
@needs_py310
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}
@needs_py310
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",
}
]
}
)
@needs_py310
@ -95,7 +116,16 @@ def test_openapi_schema(client: TestClient):
},
{
"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",
"in": "query",
},
@ -103,7 +133,19 @@ def test_openapi_schema(client: TestClient):
"requestBody": {
"content": {
"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",
"properties": {
"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"},
"description": {"title": "Description", "type": "string"},
"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"}
),
},
},
"ValidationError": {

140
tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py

@ -1,5 +1,7 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py39
@ -12,45 +14,64 @@ def get_client():
return client
item_id_not_int = {
"detail": [
{
"loc": ["path", "item_id"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
@needs_py39
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",
}
@needs_py39
@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, client: TestClient):
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"}
@needs_py39
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}
@needs_py39
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",
}
]
}
)
@needs_py39
@ -95,7 +116,16 @@ def test_openapi_schema(client: TestClient):
},
{
"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",
"in": "query",
},
@ -103,7 +133,19 @@ def test_openapi_schema(client: TestClient):
"requestBody": {
"content": {
"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",
"properties": {
"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"},
"description": {"title": "Description", "type": "string"},
"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"}
),
},
},
"ValidationError": {

140
tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py

@ -1,5 +1,7 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py310
@ -12,45 +14,64 @@ def get_client():
return client
item_id_not_int = {
"detail": [
{
"loc": ["path", "item_id"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
@needs_py310
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",
}
@needs_py310
@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, client: TestClient):
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"}
@needs_py310
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}
@needs_py310
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",
}
]
}
)
@needs_py310
@ -95,7 +116,16 @@ def test_openapi_schema(client: TestClient):
},
{
"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",
"in": "query",
},
@ -103,7 +133,19 @@ def test_openapi_schema(client: TestClient):
"requestBody": {
"content": {
"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",
"properties": {
"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"},
"description": {"title": "Description", "type": "string"},
"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"}
),
},
},
"ValidationError": {

250
tests/test_tutorial/test_body_multiple_params/test_tutorial003.py

@ -1,92 +1,147 @@
import pytest
from dirty_equals import IsDict
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(
"path,body,expected_status,expected_response",
[
(
"/items/5",
{
"importance": 2,
"item": {"name": "Foo", "price": 50.5},
"user": {"username": "Dave"},
},
200,
{
"item_id": 5,
"importance": 2,
"item": {
"name": "Foo",
"price": 50.5,
"description": None,
"tax": None,
},
"user": {"username": "Dave", "full_name": None},
},
),
(
"/items/5",
None,
422,
{
"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",
},
]
},
),
(
"/items/5",
[],
422,
{
"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_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_body_valid(client: TestClient):
response = client.put(
"/items/5",
json={
"importance": 2,
"item": {"name": "Foo", "price": 50.5},
"user": {"username": "Dave"},
},
)
assert response.status_code == 200
assert response.json() == {
"item_id": 5,
"importance": 2,
"item": {
"name": "Foo",
"price": 50.5,
"description": None,
"tax": None,
},
"user": {"username": "Dave", "full_name": None},
}
def test_post_body_no_data(client: TestClient):
response = client.put("/items/5", json=None)
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():
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")
assert response.status_code == 200, response.text
assert response.json() == {
@ -142,9 +197,27 @@ def test_openapi_schema():
"type": "object",
"properties": {
"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"},
"description": {"title": "Description", "type": "string"},
"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"}
),
},
},
"User": {
@ -153,7 +226,16 @@ def test_openapi_schema():
"type": "object",
"properties": {
"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": {

250
tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py

@ -1,92 +1,147 @@
import pytest
from dirty_equals import IsDict
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(
"path,body,expected_status,expected_response",
[
(
"/items/5",
{
"importance": 2,
"item": {"name": "Foo", "price": 50.5},
"user": {"username": "Dave"},
},
200,
{
"item_id": 5,
"importance": 2,
"item": {
"name": "Foo",
"price": 50.5,
"description": None,
"tax": None,
},
"user": {"username": "Dave", "full_name": None},
},
),
(
"/items/5",
None,
422,
{
"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",
},
]
},
),
(
"/items/5",
[],
422,
{
"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_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_body_valid(client: TestClient):
response = client.put(
"/items/5",
json={
"importance": 2,
"item": {"name": "Foo", "price": 50.5},
"user": {"username": "Dave"},
},
)
assert response.status_code == 200
assert response.json() == {
"item_id": 5,
"importance": 2,
"item": {
"name": "Foo",
"price": 50.5,
"description": None,
"tax": None,
},
"user": {"username": "Dave", "full_name": None},
}
def test_post_body_no_data(client: TestClient):
response = client.put("/items/5", json=None)
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():
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")
assert response.status_code == 200, response.text
assert response.json() == {
@ -142,9 +197,27 @@ def test_openapi_schema():
"type": "object",
"properties": {
"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"},
"description": {"title": "Description", "type": "string"},
"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"}
),
},
},
"User": {
@ -153,7 +226,16 @@ def test_openapi_schema():
"type": "object",
"properties": {
"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": {

242
tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py

@ -1,5 +1,7 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py310
@ -12,85 +14,136 @@ def get_client():
return client
# Test required and embedded body parameters with no bodies sent
@needs_py310
@pytest.mark.parametrize(
"path,body,expected_status,expected_response",
[
(
"/items/5",
{
"importance": 2,
"item": {"name": "Foo", "price": 50.5},
"user": {"username": "Dave"},
},
200,
{
"item_id": 5,
"importance": 2,
"item": {
"name": "Foo",
"price": 50.5,
"description": None,
"tax": None,
},
"user": {"username": "Dave", "full_name": None},
},
),
(
"/items/5",
None,
422,
{
"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",
},
]
},
),
(
"/items/5",
[],
422,
{
"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_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
def test_post_body_valid(client: TestClient):
response = client.put(
"/items/5",
json={
"importance": 2,
"item": {"name": "Foo", "price": 50.5},
"user": {"username": "Dave"},
},
)
assert response.status_code == 200
assert response.json() == {
"item_id": 5,
"importance": 2,
"item": {
"name": "Foo",
"price": 50.5,
"description": None,
"tax": None,
},
"user": {"username": "Dave", "full_name": None},
}
@needs_py310
def test_post_body_no_data(client: TestClient):
response = client.put("/items/5", json=None)
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
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
@ -150,9 +203,27 @@ def test_openapi_schema(client: TestClient):
"type": "object",
"properties": {
"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"},
"description": {"title": "Description", "type": "string"},
"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"}
),
},
},
"User": {
@ -161,7 +232,16 @@ def test_openapi_schema(client: TestClient):
"type": "object",
"properties": {
"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": {

242
tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py

@ -1,5 +1,7 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py39
@ -12,85 +14,136 @@ def get_client():
return client
# Test required and embedded body parameters with no bodies sent
@needs_py39
@pytest.mark.parametrize(
"path,body,expected_status,expected_response",
[
(
"/items/5",
{
"importance": 2,
"item": {"name": "Foo", "price": 50.5},
"user": {"username": "Dave"},
},
200,
{
"item_id": 5,
"importance": 2,
"item": {
"name": "Foo",
"price": 50.5,
"description": None,
"tax": None,
},
"user": {"username": "Dave", "full_name": None},
},
),
(
"/items/5",
None,
422,
{
"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",
},
]
},
),
(
"/items/5",
[],
422,
{
"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_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
def test_post_body_valid(client: TestClient):
response = client.put(
"/items/5",
json={
"importance": 2,
"item": {"name": "Foo", "price": 50.5},
"user": {"username": "Dave"},
},
)
assert response.status_code == 200
assert response.json() == {
"item_id": 5,
"importance": 2,
"item": {
"name": "Foo",
"price": 50.5,
"description": None,
"tax": None,
},
"user": {"username": "Dave", "full_name": None},
}
@needs_py39
def test_post_body_no_data(client: TestClient):
response = client.put("/items/5", json=None)
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
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
@ -150,9 +203,27 @@ def test_openapi_schema(client: TestClient):
"type": "object",
"properties": {
"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"},
"description": {"title": "Description", "type": "string"},
"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"}
),
},
},
"User": {
@ -161,7 +232,16 @@ def test_openapi_schema(client: TestClient):
"type": "object",
"properties": {
"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": {

242
tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py

@ -1,5 +1,7 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py310
@ -12,85 +14,136 @@ def get_client():
return client
# Test required and embedded body parameters with no bodies sent
@needs_py310
@pytest.mark.parametrize(
"path,body,expected_status,expected_response",
[
(
"/items/5",
{
"importance": 2,
"item": {"name": "Foo", "price": 50.5},
"user": {"username": "Dave"},
},
200,
{
"item_id": 5,
"importance": 2,
"item": {
"name": "Foo",
"price": 50.5,
"description": None,
"tax": None,
},
"user": {"username": "Dave", "full_name": None},
},
),
(
"/items/5",
None,
422,
{
"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",
},
]
},
),
(
"/items/5",
[],
422,
{
"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_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
def test_post_body_valid(client: TestClient):
response = client.put(
"/items/5",
json={
"importance": 2,
"item": {"name": "Foo", "price": 50.5},
"user": {"username": "Dave"},
},
)
assert response.status_code == 200
assert response.json() == {
"item_id": 5,
"importance": 2,
"item": {
"name": "Foo",
"price": 50.5,
"description": None,
"tax": None,
},
"user": {"username": "Dave", "full_name": None},
}
@needs_py310
def test_post_body_no_data(client: TestClient):
response = client.put("/items/5", json=None)
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
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
@ -150,9 +203,27 @@ def test_openapi_schema(client: TestClient):
"type": "object",
"properties": {
"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"},
"description": {"title": "Description", "type": "string"},
"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"}
),
},
},
"User": {
@ -161,7 +232,16 @@ def test_openapi_schema(client: TestClient):
"type": "object",
"properties": {
"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": {

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.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}
response = client.post("/index-weights/", json=data)
assert response.status_code == 200, response.text
assert response.json() == data
def test_post_invalid_body():
def test_post_invalid_body(client: TestClient):
data = {"foo": 2.2, "3": 3.3}
response = client.post("/index-weights/", json=data)
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"loc": ["body", "__key__"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"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")
assert response.status_code == 200, response.text
assert response.json() == {

35
tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py

@ -1,5 +1,7 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py39
@ -25,15 +27,30 @@ def test_post_invalid_body(client: TestClient):
data = {"foo": 2.2, "3": 3.3}
response = client.post("/index-weights/", json=data)
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"loc": ["body", "__key__"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"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

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 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")
assert response.status_code == 200, response.text
assert response.json() == {
@ -17,7 +23,7 @@ def test_get():
}
def test_put():
def test_put(client: TestClient):
response = client.put(
"/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")
assert response.status_code == 200, response.text
assert response.json() == {
@ -118,9 +124,36 @@ def test_openapi_schema():
"title": "Item",
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"description": {"title": "Description", "type": "string"},
"price": {"title": "Price", "type": "number"},
"name": IsDict(
{
"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},
"tags": {
"title": "Tags",

34
tests/test_tutorial/test_body_updates/test_tutorial001_py310.py

@ -1,4 +1,5 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
@ -128,9 +129,36 @@ def test_openapi_schema(client: TestClient):
"title": "Item",
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"description": {"title": "Description", "type": "string"},
"price": {"title": "Price", "type": "number"},
"name": IsDict(
{
"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},
"tags": {
"title": "Tags",

34
tests/test_tutorial/test_body_updates/test_tutorial001_py39.py

@ -1,4 +1,5 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py39
@ -128,9 +129,36 @@ def test_openapi_schema(client: TestClient):
"title": "Item",
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"description": {"title": "Description", "type": "string"},
"price": {"title": "Price", "type": "number"},
"name": IsDict(
{
"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},
"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 docs_src.conditional_openapi import tutorial001
from ...utils import needs_pydanticv1
def test_disable_openapi(monkeypatch):
monkeypatch.setenv("OPENAPI_URL", "")
def get_client() -> TestClient:
from docs_src.conditional_openapi import tutorial001
importlib.reload(tutorial001)
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")
assert response.status_code == 404, response.text
response = client.get("/docs")
@ -17,16 +28,19 @@ def test_disable_openapi(monkeypatch):
assert response.status_code == 404, response.text
# TODO: pv2 add version with Pydantic v2
@needs_pydanticv1
def test_root():
client = TestClient(tutorial001.app)
client = get_client()
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
# TODO: pv2 add version with Pydantic v2
@needs_pydanticv1
def test_default_openapi():
importlib.reload(tutorial001)
client = TestClient(tutorial001.app)
client = get_client()
response = client.get("/docs")
assert response.status_code == 200, response.text
response = client.get("/redoc")

12
tests/test_tutorial/test_cookie_params/test_tutorial001.py

@ -1,4 +1,5 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.cookie_params.tutorial001 import app
@ -56,7 +57,16 @@ def test_openapi_schema():
"parameters": [
{
"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",
"in": "cookie",
}

12
tests/test_tutorial/test_cookie_params/test_tutorial001_an.py

@ -1,4 +1,5 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.cookie_params.tutorial001_an import app
@ -56,7 +57,16 @@ def test_openapi_schema():
"parameters": [
{
"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",
"in": "cookie",
}

12
tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py

@ -1,4 +1,5 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
@ -62,7 +63,16 @@ def test_openapi_schema():
"parameters": [
{
"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",
"in": "cookie",
}

12
tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py

@ -1,4 +1,5 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py39
@ -62,7 +63,16 @@ def test_openapi_schema():
"parameters": [
{
"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",
"in": "cookie",
}

12
tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py

@ -1,4 +1,5 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
@ -62,7 +63,16 @@ def test_openapi_schema():
"parameters": [
{
"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",
"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.utils import match_pydantic_error_url
from docs_src.custom_request_and_route.tutorial002 import app
@ -12,16 +14,33 @@ def test_endpoint_works():
def test_exception_handler_body_access():
response = client.post("/", json={"numbers": [1, 2, 3]})
assert response.json() == {
"detail": {
"body": '{"numbers": [1, 2, 3]}',
"errors": [
{
"loc": ["body"],
"msg": "value is not a valid list",
"type": "type_error.list",
}
],
assert response.json() == IsDict(
{
"detail": {
"errors": [
{
"type": "list_type",
"loc": ["body"],
"msg": "Input should be a valid 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.utils import match_pydantic_error_url
from docs_src.dataclasses.tutorial001 import app
@ -19,15 +21,30 @@ def test_post_item():
def test_post_invalid_item():
response = client.post("/items/", json={"name": "Foo", "price": "invalid price"})
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"loc": ["body", "price"],
"msg": "value is not a valid float",
"type": "type_error.float",
}
]
}
assert response.json() == IsDict(
{
"detail": [
{
"type": "float_parsing",
"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():
@ -88,8 +105,26 @@ def test_openapi_schema():
"properties": {
"name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"},
"description": {"title": "Description", "type": "string"},
"tax": {"title": "Tax", "type": "number"},
"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"}
),
},
},
"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 docs_src.dataclasses.tutorial002 import app
@ -20,8 +21,7 @@ def test_get_item():
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
data = response.json()
assert data == {
assert response.json() == {
"openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
@ -51,13 +51,42 @@ def test_openapi_schema():
"properties": {
"name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"},
"tags": {
"title": "Tags",
"type": "array",
"items": {"type": "string"},
},
"description": {"title": "Description", "type": "string"},
"tax": {"title": "Tax", "type": "number"},
"tags": IsDict(
{
"title": "Tags",
"type": "array",
"items": {"type": "string"},
"default": [],
}
)
| 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 docs_src.dataclasses.tutorial003 import app
@ -135,11 +136,22 @@ def test_openapi_schema():
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"items": {
"title": "Items",
"type": "array",
"items": {"$ref": "#/components/schemas/Item"},
},
"items": IsDict(
{
"title": "Items",
"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": {
@ -159,7 +171,16 @@ def test_openapi_schema():
"type": "object",
"properties": {
"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": {

23
tests/test_tutorial/test_dependencies/test_tutorial001.py

@ -1,4 +1,5 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.dependencies.tutorial001 import app
@ -52,7 +53,16 @@ def test_openapi_schema():
"parameters": [
{
"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",
"in": "query",
},
@ -102,7 +112,16 @@ def test_openapi_schema():
"parameters": [
{
"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",
"in": "query",
},

23
tests/test_tutorial/test_dependencies/test_tutorial001_an.py

@ -1,4 +1,5 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.dependencies.tutorial001_an import app
@ -52,7 +53,16 @@ def test_openapi_schema():
"parameters": [
{
"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",
"in": "query",
},
@ -102,7 +112,16 @@ def test_openapi_schema():
"parameters": [
{
"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",
"in": "query",
},

23
tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py

@ -1,4 +1,5 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
@ -60,7 +61,16 @@ def test_openapi_schema(client: TestClient):
"parameters": [
{
"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",
"in": "query",
},
@ -110,7 +120,16 @@ def test_openapi_schema(client: TestClient):
"parameters": [
{
"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",
"in": "query",
},

23
tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py

@ -1,4 +1,5 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py39
@ -60,7 +61,16 @@ def test_openapi_schema(client: TestClient):
"parameters": [
{
"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",
"in": "query",
},
@ -110,7 +120,16 @@ def test_openapi_schema(client: TestClient):
"parameters": [
{
"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",
"in": "query",
},

23
tests/test_tutorial/test_dependencies/test_tutorial001_py310.py

@ -1,4 +1,5 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
@ -60,7 +61,16 @@ def test_openapi_schema(client: TestClient):
"parameters": [
{
"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",
"in": "query",
},
@ -110,7 +120,16 @@ def test_openapi_schema(client: TestClient):
"parameters": [
{
"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",
"in": "query",
},

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

Loading…
Cancel
Save